Philosophy

We don’t need a ternary operator

A staple of compact code, the ternary operator ?: feels like it’s earned a place 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

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

//unary operators
-a
~bits
!cond

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

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()

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()

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()

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;

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)))

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;

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`

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 

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
int a = cond ? true_value : false_value;

? 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.

5 replies »

  1. I like the way Scala handles it. In Scala the if-statement returns a value, so you can do:
    val y = …
    val x = if (y < 5) { 5 } else { y + 22 }

    • It’s a different syntax, yes, less overhead — I was allowing similar in Leaf as well with expressions.

      It doesn’t allow just `if (y < 5) { 5 }` to exist on its own, which is what I'd find nice.

  2. How about Lua?

    Lua 5.3.3 Copyright (C) 1994-2016 Lua.org, PUC-Rio
    > a or 1
    1
    > b = 2
    > b or 3
    2
    > true and 4
    4
    > false and 5
    false
    > true and 6 or 7
    6
    > false and 8 or 9
    9
    > true and 0 or 10
    0
    — note that 0 evaluates to true
    — only false and nil evaluate to false
    > true and false or 11
    11
    — so you cannot use the above to generate false (or nil)
    > true and false or nil
    nil
    > true and nil or false
    false

  3. SQL has a different ternary operator: x BETWEEN y AND z. This is somewhat useful, but I think the Python way of writing it, mentioned in the article, is pretty clearly superior (it’s n-ary rather than ternary; it’s explicit about < vs <=; the arguments are in logical order; it actually looks like normal mathematical notation; …).

    SQL also has a conditional operator, but that's n-ary rather than ternary:
    CASE
    WHEN a THEN b
    WHEN c THEN d

    ELSE z
    END
    I think every language should just have something like that instead of all these less expressive ternary conditionals.

    Also, unless I'm misunderstanding something, I think this gives unexpected results (for those trying to replace conditional operators):
    var a : optional
    var b = true
    print(b ? a : "b was false!") // prints nothing
    print(b ? a | "warning: b was false!") // prints "warning: b was false!"

    • Yes, that second example has come up in other places as well. Given the ambiguity of what would happen that would be rejected by the language. You can’t have a conditional return an optional value as part of the expression — the compiler would flag this as an error.

      It makes sense since `b ? a` is attempting to wrap an optional around an existing optional. In Leaf that wasn’t allowed. I didn’t see any good use-cases that would require it. Forbidding it simplifies the language and removes the ambiguity.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s