DEV Community

Cover image for We don't need a ternary operator
edA‑qa mort‑ora‑y
edA‑qa mort‑ora‑y

Posted on • Originally published at mortoray.com

We don't need a ternary operator

A staple of compact code, the ternary operator ?: feels like it's earned a place in any programming language. I use it often; you should use it often. However, I don't like the operator itself. It feels complex and incomplete. Though I've stopped development on Leaf, I did find a better option.

Let's take a deeper look at what this ternary operator is, then show how a language could avoid it.

What's this ternary operator?

The name of this operator raises questions. A ternary operator, in a typical math sense, or from the view of a parser, is an operator that takes three arguments. An operator is a function that has a special syntax, not the generic call syntax foo(a,b,c). Most common are binary operators, we see them everywhere.

//binary operators
x + y
x / y
x * y
Enter fullscreen mode Exit fullscreen mode

There are also a handful of unary operators we see commonly. Unary meaning they have only one argument.

//unary operators
-a
~bits
!cond
Enter fullscreen mode Exit fullscreen mode

There's no shortage of unary and binary operators. But what examples of ternary operators do we have?

//ternary operators
cond ? true_value : false_value
Enter fullscreen mode Exit fullscreen mode

Are you scratching your head trying to think of another one? As far as I know, there aren't any. This is why we end up calling this the "ternary operator" instead of using a proper name, like "conditional operator". There aren't any other ternary operators in programming languages.

Chained Operators

Let's step aside for a second. There are operator sequences and combinations that may look like ternary operators but aren't.

For example, this Python comparison appears to involve three arguments.

if a < b < c:
    go()
Enter fullscreen mode Exit fullscreen mode

That looks similar to a ternary operator. It must consider all three arguments to evaluate correctly.

However, digging deeper this is more of a syntactic sugar than a true ternary operator. It's equivalent to the following.

once_b = b
if a < once_b and once_b < c:
    go()
Enter fullscreen mode Exit fullscreen mode

You may find several examples that show only a < b and b < c, but those are incorrect. The original form guarantees that b is evaluated only once. I use the once_b = b to show this guarantee; I assume a different internal feature achieves this in practice.

This python construct can be extended to include even more values.

if a < b < c < d <e < e:
    go()
Enter fullscreen mode Exit fullscreen mode

It's a chained operator. There's no limit to what can be added.

The << stream operator in C++ can also be chained.

cout << "Hello " << first_name << " " << last_name << endl;
Enter fullscreen mode Exit fullscreen mode

Unlike Python's chained comparisons, this C++ sequence doesn't require any parser support. << is a normal binary operator that happens to be chainable. Basic math has this same property, for example a + b + c + d.

Messy syntax and parsing

I said there aren't any other ternary operators in programming languages. There's a good reason for this.

Look at cond ? true_value : false_value. Is there anything that makes it look like three arguments are involved? What if I change the operators slightly, to unknown ones: expr ⋇ value_a ⊸ value_b. That looks like two binary operators. Even if I keep the same symbols, but add nested expressions, it looks off: cond ? value_a + value_b : value_c * value_d. There's no visual indication that ? ... : is one unified operator. This syntax limitation prevents any new ternary operators from being introduced. It'd create a backlash. ?: is already in some coder's bad books as it's easy to abuse.

Despite providing a valuable syntax, the ternary operator is syntactically messy. It's just weird to have two split symbols define three arguments. A function with three arguments is not a problem since we have a robust syntax for it foo( a, b, c ).

Adding to the complexity is precedence. All operators require precedence. For example, a + b * c is evaluated as a + (b*c). Multiplication has higher precedence than addition. Its result is evaluated before addition.

Operators of the same precedence also have associativity. For example 5 - 2 + 3 is evaluated as (5 - 2) + 3 = 6, which is left associative. Right associativity would be evaluated as 5 - (2 + 3) = 0, which is wrong.

Right associativity is generally reserved for unary operators, which only have a right argument, and assignment operators. For example, if you're crazy enough to write a = b += c, right associativity evaluates this as a = (b += c).

The ternary operator is right associative. It doesn't feel right though. How can an operator with three arguments and two symbols have associativity defined merely as left or right?

cond_a ? val_one : cond_b ? val_two : val_three

//right? associative
cond_a ? val_one : (cond_b ? val_two : val_three)

//left? associative (wrong)
(cond_a ? val_one : cond_b) ? val_two : val_three

//strict-left? associative (wacky)
(((cond_a ? val_one) : cond_b) ? val_two) : val_three

//strict-right? associative (nonsensical)
cond_a ? (val_one : (cond_b ? (val_two : val_three)))
Enter fullscreen mode Exit fullscreen mode

The two strict forms, which apply associativity on each operator symbol, are wacky. Think for a moment from the point of the view of the parser. It's one of those two that make the most sense syntactically. The first two forms require a bit of trickery to bypass regular associative parsing.

Try to imagine what happens in nested ternary operators: a ? b ? c : d : e. You won't find nested ternaries often in code. They are too hard to parse mentally.

You find lots of code depending on the quasi-right associativity. That's what allows chaining.

int a = cond_a ? val_one :
    cond_b ? val_two :
    cond_c ? val_three : val_four;
Enter fullscreen mode Exit fullscreen mode

A binary solution

When I was working on Leaf, I wanted to avoid the ternary operator. I liked the feature it provided, but I wanted to find a more fundamental approach to providing it.

The solution came in the form of language optionals. These were fundamental in Leaf, thus had operator support.

The first operator was a defaulting one. If the optional value were set, it'd evaluate to that. Otherwise, it'd evaluate to the provided default.

var a : optional
print( a | 1 )  //prints 1, since `a` has no value

var b : optional = 2
print( b | 3 )  //prints 2, since that's the value in `b`
Enter fullscreen mode Exit fullscreen mode

The || operator in JavaScript achieves the same effect in many cases. In C# there is a null coalescing operator ?? that uses null in the same way as Leaf uses unset optionals.

This sounds like half of what the ternary operator does. We could look at like this: cond ? some_val | default_val. That is, consider the entire left part, the cond ? some_val to produce an optional value. Given an optional, we already have operators to default that value when unset.

Leaf incorporated the ? operator to create an optional.

var a = true ? 5  //a is an optional set to value 5
var b = false ? 6  //b is an optional without a value 
Enter fullscreen mode Exit fullscreen mode

On its own, this operator is often useful for optionals. Combined with | default operator it mimics the traditional ternary operator.

var a = cond ? true_value | false_value
Enter fullscreen mode Exit fullscreen mode
int a = cond ? true_value : false_value;
Enter fullscreen mode Exit fullscreen mode

? and | are both binary operators. There is no need to have an actual ternary operator to produce the same conditional evaluation. We get the same feature without the syntactic messiness. It's a lot clearer what happens, and the parser doesn't require any trickery.

It also improves expressiveness without introducing more symbols, as both operators are useful on their own.

Alas, I've not seen any plans to add this to any language. If you know of one, please leave a comment. I'd be curious to see it.

Top comments (59)

Collapse
 
joelnet profile image
JavaScript Joel

Great writeup.

I do love the ternary operator. I can see how others get confused though. In an application where everything is written one way (regardless of the way) and another way is introduced, it will add confusion.

Though if your application's style guide enforces the use of the ternary operator, it becomes idiomatic.

const getFee = isMember =>
  isMember ? "$2.00" : "$10.00"

const canDrinkAlcohol = age =>
  age >= 21 ? "True, over 21" : "False, under 21"

const isOld => age =>
  age >= 40 ? "You are never too old!" : "Nope"

// YUCK. inconsistent.
function canDrive(age) {
  if (age >= 16) {
    return "Yes"
  } 
  return "Nope"
}

The key to being idiomatic is is consistency. This can be made true even with a nested ternary!

Collapse
 
glosswater profile image
lipgloss

To me, that style with the ternary is a little frustrating because it feels like it's so close to someone embracing a more functional style. It's not that ternary can't exist or be useful in that case, but it just seems like a tease at that point. Like, to take your example and convert it into an admittedly contrived example, it might look something like:


const greaterThanOrEq = left => right => left >= right;
const explainDecision = decision => successMessage => failureMessage =>
    (decision && successMessage) || failureMessage;
const canDrinkAlcohol = age => greaterThanOrEq(age)(21);
const isOld = age => greaterThanOrEq(age)(40);
const canIDrink = age => explainDecision(canDrinkAlcohol(age))("yes")("no");
const amIOld = age => explainDecision(isOld(age))("You are never too old!")("Nope");
console.log(canIDrink(22));
console.log(canIDrink(19));
console.log(amIOld(45));
console.log(amIOld(39));
Collapse
 
joelnet profile image
JavaScript Joel

LOL I feel the exact same way. The Ternary feels like an in-between. Somewhere in the middle of imperative and functional.

Today I was actually working on a style guide for a project I'm working on. It looks exactly like the code you created.

const getOrdersText = ifElse (hasOrders) (orderCountText) (noOrderCountText)

It's not quite ready for human consumption, but I think you would find it interesting. Hit me up on Twitter if interested in a sneak peek.

Cheers!

Thread Thread
 
qm3ster profile image
Mihail Malo

The problem with this is that both orderCountText and noOrderCountText will be evaluated in either case.
Do they have to be functions?

Thread Thread
 
joelnet profile image
JavaScript Joel

The problem with this is that both orderCountText and noOrderCountText will be evaluated in either case.

only one function will execute in this case, either the if case or the else case, but never both.

In my version of ifElse, they can be values that will be returned or functions to be executed.

Thread Thread
 
qm3ster profile image
Mihail Malo

Yeah, whereas in condition ? expensiveGet() + 1 : expensiveGet2() - 1 only one expression will be evaluated.

Thread Thread
 
joelnet profile image
JavaScript Joel

Same with ifElse

Thread Thread
 
joelnet profile image
JavaScript Joel • Edited
const expensiveGetPlus = x => y => expensiveGet(y) + x

ifElse (condition) (expensiveGetPlus (1)) (expensiveGetPlus (-1)) (true)
Thread Thread
 
joelnet profile image
JavaScript Joel

Check out Sanctuary's ifElse sanctuary.js.org. You can live edit their page to see how it works.

I'd love to get some live editing docs page for my stuff too. Pretty neat site.

Thread Thread
 
qm3ster profile image
Mihail Malo

I see it's actually for threading through the value, just like tap:

S.ifElse(x=>typeof x === "number")(x=>x-1)(x=>x+"a")(3)
 2

S.ifElse(x=>typeof x === "number")(x=>x-1)(x=>x+"a")("banana")
 "bananaa"

However, for normal conditional use it's error prone, as you've shown in your code. Both expensive gets will run before the condition is even applied to true.
The actual code would be

const expensiveGetPlus = x => () => expensiveGet() + x
const expensiveGet2Plus = x => () => expensiveGet2() + x

ifElse (condition) (expensiveGetPlus (1)) (expensiveGet2Plus (-1)) (true)
Thread Thread
 
joelnet profile image
JavaScript Joel

That's correct, ifElse accepts functions. So those functions are only executed when the condition is met.

Thread Thread
 
joelnet profile image
JavaScript Joel

Both expensive gets will run before the condition is even applied to true.

This is still false. Only one will run. Never both.

const isEven = n => n % === 0
const logEven = n => console.log(`${n} is Even!`)
const logOdd = n => console.log(`${n} is Odd`)

ifElse (isEven) (logEven) (logOdd) (10)

//=> "10 is Even!"

You can see logOdd is never called.

Thread Thread
 
qm3ster profile image
Mihail Malo

In your first example

const expensiveGetPlus = x => expensiveGet() + x

ifElse (condition) (expensiveGetPlus (1)) (expensiveGetPlus (-1)) (true)

both run.

const expensiveGet = () => {
  console.log("doing expensive get")
  return 2
}
Thread Thread
 
joelnet profile image
JavaScript Joel

Yes you are correct. The expensiveGet method needs to take 2 arguments for it to work the way it is being called in ifElse.

I have created a working example that you can run.

const S = require('sanctuary')

const expensiveGet = (y) => {
  console.log("doing expensive get for", y)
  return 2
}
const condition = x => x === 100
const expensiveGetPlus = x => y =>
  expensiveGet(y) + x

S.ifElse (condition) (expensiveGetPlus (1))
  (expensiveGetPlus (-1)) (100)
//=> "doing expensive get for 100"

This example will show that ifElse only executes one of the functions.

Collapse
 
skuzzle profile image
Simon Taddiken • Edited

Here is another read up about ternaries explaining why/how they can be great even when being nested: medium.com/javascript-scene/nested...

Collapse
 
jeikabu profile image
jeikabu

I agree with with all your points.

But I feel like there's situations where it's arguably clearer than the alternative.

Other commenters have mentioned the functional angle, and that's really it. In many (all?) the languages that have the ternary operator if is a statement (rather than an expression). Make if an expression and ternary has no place in the world:

let a = if cond { true_value } else { false_value };
Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

Note, I say we don't need the ternary because it can be replaced with general purpose binary operators. I'm not suggesting to get rid of the functionality, that'd be crazy-talk.

Collapse
 
benjaminblack profile image
Benjamin Black • Edited

I've found one good use for the ternary operator: clearly and succinctly transforming a callback into a promise:

let result = await new Promise((resolve, reject) => {
  someOldFunc(..., (error, data) => error ? reject(error) : resolve(data));
});

But even still, the ternary operator does not pass the Crockford test.

"If a feature is sometimes dangerous, and there is a better option, then always use the better option."

Collapse
 
glosswater profile image
lipgloss

You could still do that without the ternary and in the same space:

error && reject(error) || resolve(data)
Collapse
 
maxart2501 profile image
Massimo Artizzu

No, just... no.
The ternary operator is better than logical operator abuse.

Thread Thread
 
glosswater profile image
lipgloss

how is making a binary decision operator abuse?

Thread Thread
 
maxart2501 profile image
Massimo Artizzu

Because what you actually want is an if-then-else path. The ternary operator has been conceived for that. Logical operators are for logical expression.

Sure you can use them to create conditional branching, but that doesn't make that any clearer. It's hiding the broken glass under the rug (i.e., not using the ternary operator for the sake of not using it, not because you're opting for a clearer pattern).

Thread Thread
 
glosswater profile image
lipgloss • Edited

Fair enough regarding clarity, though in that case we could say that the given example is at fault and not how we decide to return the result ;). It's true that rather than check if error is truthy, it'd be nicer to write something like reject(error) || resolve(data). (I know that doesn't make sense for promise resolution, but bear with me as it's just the example we're working with.)

Regarding "logical operators are for logical expression", the ternary operator's left hand side expression is already a logical expression by nature. Adding a syntax for deciding whether to go "left or right" that doesn't follow the same rules as any other operator in the language and worse, different from the ternary implementations in other, older languages can add confusion where there doesn't need to be.

All that aside, I love the ternary operator when writing, but during reviews, or walking people through later, what seemed so simple to me at the time of writing is rarely as clear. The logical or's result is immediately clear.

A pattern I also see (specifically in new React developers, I think) is returning null as the right hand operand for the ternary. I think it's a weird case of "the ternary operator is cool and I want my code to be concise". That added layer of "need to actually learn the ternary operator to write it" creates a desire to use and abuse it.

Edit, for clarity and stuff: As with everything in programming there's a time and place for everything. But if your goal is to have a single line, immediate "true or false" result of something, I feel the logical or is made for that. There's a small window where the ternary probably doesn't hurt anything but is ultimately the same as using multiple logical ors. Anything much larger or nested probably calls for actual if/else.

Collapse
 
shalvah profile image
Shalvah • Edited

I like the ternary. Like @fnh pointed out, it's the only way of doing direct conditional assignment in most languages. And for the most part, we've conditioned ourselves to how it works.

But I have a hard rule: never nest a ternary. No matter how clever it makes me seem, or how many articles I read on functional programming and why ternaries are awesome, I won't. The stress it puts other programmers (and myself!) through to mentally parse a nested ternary isn't worth it.

Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

Yes, nesting of the second argument, the truth value, can be hideous. Nesting, or better called chaining, of the third argument can be acceptable with good formatting.

Collapse
 
lexlohr profile image
Alex Lohr

You basically just replaced condition ? if_true : if_false with condition && if_true || if_false - that means that a falsy if_true will fall through to if_false.

I don't really see the advantage of that solution.

Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

No, because I'm not allowing "falsey" conditions, which I consider a failure in a language. false is a distinct value from an unset optional, and true is not the same as an optional with a value.

Having hard optional semantics allows this to work. Using truthy and falsey evaluations this fails.

Collapse
 
lexlohr profile image
Alex Lohr

A condition can obviously be false, but I guess that's not what you meant. For the sake of this argument, let's assume that condition c is true, value x is false and y is 1; in most languages, c ? x : y would result in false, whereas c && x || y would result in 1.

If I understood you correctly, c ? false | 1 in leaf would too result in 1, thus breaking the original logic of a ternary operator.

Thread Thread
 
mortoray profile image
edA‑qa mort‑ora‑y

No, c ? false | 1 would evaluate to false in Leaf. The | operator applies only to optional types, not truthy values.

Collapse
 
bosepchuk profile image
Blaine Osepchuk

Great post.

As I was reading it, I was just thinking about how messed up programming is.

I personally hate the ternary operator. It's a conditional disguised as an assignment and when I scan code, I often read it wrong. So, we removed it from all our code and created an inspection to make sure it never gets back in.

Totally personal preference here, but an if else is much more readable.

I never even considered all the edge cases you brought up or that it works differently in different languages. Do other programming language developers secretly hate programmers (only half kidding)?

Collapse
 
dfeuster profile image
Daniel Feuster

@Blaine I like to think of all the various programming languages as exactly that, languages. Imagine if you will, that C is latin. Its the root of most modern "western" languages (for brevity sake). Each language that sprouts from it has syntax that is similar but more refined and easier to use in particular circumstances that are important to that language. Then within that language itself there are deeper refinements, these are the regional dialects. It's actually quite interesting.

Collapse
 
bosepchuk profile image
Blaine Osepchuk

Yes, I'm aware. The research shows that C-like language syntax is basically as hard to learn as random syntax. That's not good. It makes programming harder than it needs to be. And every time I see some crazy-dense and indecipherable code I wonder what we're really doing here. Are we trying to be cryptographers, jerks, or what?

Programming is already cognitively demanding. We should be saving or brain cycles for our programs, not trying to remember all the obscure edge cases of the ternary operator in C.

Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

If...Else statements are flow though. I don't like using them in situations where I'm alternating between expression values. I let the high-level logic usually decide whether I want an if-else or a conditional-operator. If I use the wrong one it will look odd.

Collapse
 
bosepchuk profile image
Blaine Osepchuk

That's a fair point. In my code base, the previous programmers used them indiscriminately, which was part of the reason we're got rid of them. I've seen that pattern of usage in open source projects too.

If you used them consistently in the manor you've described, I could get on board with it.

Collapse
 
fnh profile image
Fabian Holzer

I think the ternary is mostly a symptom of the design decision to distinguish between expressions and statements, which many programming languages share. When you could just assign the value of an if or switch/case, as it is the case for example in Ruby, there would be much less need for a ternary operator and especally to nested tenarys.

When you considered the optional type for Leaf, did you have a type that abides the definition for monadic types in mind (e.g. something like the Maybe type in Haskell), to handle null values more gracefully, or was it more a construct for the sake of a simpler grammar?

Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

I had an optional type, which is what these operations were based on. Optional was an essential part of the language, as nothing was allowed to not contain a value otherwise: all defaults were defined, and there weren't null pointers.

Collapse
 
gmartigny profile image
Guillaume Martigny

IMO (in JS at least), binary operator use for non-binary are good if you know what you're dealing with.

const a = b || default; // Fallback to default if b is not set or falsy !!!

const a = condition && b; // Take the value of b when condition is truthy

const a = (condition && b) || c; // Yay, one-line conditionnal
 // Take the value of b if truthy or fallback to c if falsy

I took care to write "falsy" and "truthy" because:

return data.filter(something) || "fallback"; // never get to fallback

But when filter is empty, it should have took "fallback" ?

Yes, but empty array [] is truthy.

But you should never care about the number of lines you write. Ninja coder can write this methods in 1 lines ? Whatever, I'll use 10 lines and have a clear code-base.

const filteredBySomething = data.filter(something);
if (!filteredBySomething.length) {
  return "fallback";
}
return filteredBySomething;
Collapse
 
sanderintveld profile image
Sander in 't Veld

But in (condition && b) || c, if condition were true and b were false (or zero), it would return c. So that's not a correct implementation of a ternary operator.

Collapse
 
gmartigny profile image
Guillaume Martigny

Indeed, that's part of my point. You can do without ternary with some trickery, but best solution is to split it into real if else over multiple lines.

Collapse
 
qm3ster profile image
Mihail Malo • Edited

Noted that this is already commented elsewhere, but your binary conditional operator seems a lot like this logic operator abuse in JS:

condition && positive || negative
// ===
(condition && positive) || negative
// ===
condition ? positive : negative

And whenever I see the former I rewrite it as the latter (because it is more semantic, not because I am obsessed with removing 2 characters)

(Also, a falsy positive would cause negative to be evaluated and returned)

Collapse
 
kspeakman profile image
Kasey Speakman • Edited

A lot of the same issues you point out could also apply to if statements in general, not just the ternary operator. It's an interesting thought, because in other languages there are alternatives to using if statements. SmallTalk booleans were objects which exposed the messages ifTrue and ifFalse, rather than requiring a special if statement. The FP equivalent would be pattern matching, which maps data patterns to functions/values. Assembly has conditional jumps. All of these are conceptually similar in usage -- run this code when this condition is present.

Could it be that the deeper problem here is the if statement itself? (Since ternary is just an expression + operator form of this.) This is an honest question for discussion, since I use if at times and it is considered a staple programming construct.

Collapse
 
mortoray profile image
edA‑qa mort‑ora‑y

I think if statements are something separate, assuming the ternary isn't being abused to introduce logic flow into a program.

if statements are about flow, adn there's simply no way to get rid of them. Sure, in many cases we have higher level things, like select, or even dynamic dispatch, but the essential conditional logic persists.

Collapse
 
kspeakman profile image
Kasey Speakman • Edited

Most languages use if for flow control so they encourage you to write boolean reductions for your data and to think of your decisions in terms of booleans. Whereas my human brain doesn't tend to actually think in Discrete Logic. Sometimes I even draw truth tables to make sure it will produce the expected result. The problem isn't really with the language feature provided by if, but perhaps with our overuse of the construct.

So I guess the fundamental question is: does using if reduce the expressiveness of your solution? And if so, what are the alternatives?

When I am writing a guard clause, if's expressiveness is probably fine. But when I am making business decisions, using if for flow can feel pretty bad at times. As far as the 2nd question, I think really the only alternative to using if is to write higher level code so it is not needed. Those implementations will vary based on the problem and paradigm. Examples I've used: union types or shallow inheritance to make discrete data states.

As far as the relationship, ternary is just an expression + operator version of if. Being an operator makes it more cryptic, but I consider it being an expression to be a good thing. I tend to write in expression-based languages, so in my mind it is the exact same capability as if. (In F#, if is an expression and no ternary operator exists.) From that perspective, putting "too much" in the ternary operator feels appropriately bad because it is highlighting problems with expressing solutions in terms of booleans. Whereas the same code with a statement-based if may feel more comfortable than it should. Not that I am in favor of ternary. I think your alternative is better. I guess I was just isolating a problem that most people easily identify with ternary, but is less obvious with if. That being the lack of human legibility of boolean-based flow control.