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
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.
Latest comments (59)
First, pretty well all of the Lisp family have a ternary operator ... the 'if' expression. The observation that the true/false branch operatoronly exists in languages with if-statements is apposite. The people at Bell Labs were mathematicians, and probably felt quite comfortable with the idea of expressions with conditional paths. I am fairly sure that that is their intended purpose.
It is easy to write inscrutable code in any language, and many of the examples of cryptic conditionals we encounter are cryptic because they aren't adequately described.
Knuth's whole aim with 'literate programming' was to make everything clear, and if he can make chunks of raw TeX understandable to a novice (he does), then we can certainly make our conditional expressions intelligible.
It is true that we don't NEED the operator, but why do we NEED anything more that machine code? ... It is because, correctly used, these languages and their constructs make things clearer.
... And part of that 'correctly' is adequate commenting.
Oh, I definitely think we need the functionality. I think part of my intent of the article didn't come across. I mean we don't need a ternary operator because we could instead define two binary operators which are more generic, and also fulfill the conditional evaluation expression.
My concern about parsing is strictly due to the ternary syntax. Once we have binary operators it can be parsed, by machine and humans, the same way as other binaries and thus be even easier to understand.
As usual, the Adaist is different :-)
Ada (since 2005, I guess) has a "kind of" ternary operator that has some similarity with Ruby. In Ada you use just a "if" construct
X := (if (a > b) then 4 else 5);
If I am not wrong (I am to lazy to start the IDE and try...) you need the parenthesis around the if. I did not check thoroughly, but I think that this prevent the issues with associativity: "if" expressions have no associativity and you need parenthesis to use them. (Ada also force you to use parenthesis in expressions with both "and" and "or," in order to avoid ambiguities).
The nice part is that there is also a "case" expression
X := case day is
when Monday => 3,
When Sunday => 4,
...
end case;
They are very convenient, especially in inline functions.
This is exactly why I like Python's ternary operator:
The use of English words instead of cryptic symbols reduces the ambiguity.
Python's ordering is backwards. IT breaks the standard if-then logic and chooses then-if wording. I really dislike the Python way of handling this.
That's interesting, I tend to think about conditionals the same way it is in Python, ie "do this thing if something is true otherwise do this other thing", rather than "if something is true do this thing otherwise do this other thing".
It's interesting how different people think differently. :)
Actually I like ternary operator a lot, and I really feel it's missing on Kotlin, ever since I've moved from Java.
The way to do it on Kotlin is just annoying to write .
Compare this on Java:
final int t = x>0 ? 0 : 1 ;
with this on Kotlin
val t = if (x>0) 0 else 1
Yes, I feel using the
if ... else
syntax for an expression evaluation is harder to read than? ... :
.And in Kotlin! Although Kotlin doesn't have a ternary operator.
It has the Elvis Operator
?:
which behaves like C's ternary operator when you omit the middle value... So it's the same as C#'s coallessing operator (??
) but it has a groovier syntax!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 messagesifTrue
andifFalse
, rather than requiring a specialif
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 useif
at times and it is considered a staple programming construct.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.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 byif
, 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, usingif
for flow can feel pretty bad at times. As far as the 2nd question, I think really the only alternative to usingif
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 asif
. (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-basedif
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.You basically just replaced
condition ? if_true : if_false
withcondition && if_true || if_false
- that means that a falsyif_true
will fall through toif_false
.I don't really see the advantage of that solution.
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, andtrue
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.
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, valuex
isfalse
andy
is1
; in most languages,c ? x : y
would result infalse
, whereasc && x || y
would result in1
.If I understood you correctly,
c ? false | 1
in leaf would too result in1
, thus breaking the original logic of a ternary operator.No,
c ? false | 1
would evaluate tofalse
in Leaf. The|
operator applies only to optional types, not truthy values.Noted that this is already commented elsewhere, but your binary conditional operator seems a lot like this logic operator abuse in JS:
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 causenegative
to be evaluated and returned)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). Makeif
an expression and ternary has no place in the world: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.
Speaking of python, there are conditional expressions docs.python.org/2.5/whatsnew/pep-3...
which is, I think, the only valid use case for ternaries: providing initial values based on a condition.
Apart from that a simple if clause to return early does the trick. No need for nesting ternaries.