I’m reconsidering the necessity of the conditional ternary operator in Leaf. I’m having troubles coming up with a comfortable, flexible, and unambiguous syntax. The traditional short ‘cond ? true_value : false_value’ syntax does seem appealing, but is it a necessary shorthand?
Note my potential decision to drop this ternary operator is contingent on already having an n-ary conditional syntax. That is, Leaf has another syntax which allows selecting a value conditionally from a list of arbitrary length.
q = select [ s == 0 ? a, s == 1 ? b, else c ]
There are languages that already have this feature: I can think of Haxe which allows ‘switch’ to resolve to a value. It is essential that a language has a way to do this, lest it be considered incomplete. Any language with this notation can also do the ternary conditional, albeit a bit bulkier. The question is whether a shorthand notation for the binary case is required.
q = select [ cond ? true_value, else false_value ]
Uses of the operator
I scanned through my code, and some other projects, looking at how frequently the ternary conditional is used, and for what purposes. If you have other scenarios you find important, then please leave a comment so I can consider them.
I found a lot of places where it is used to constrain a value to a minimum or maximum.
a = length > limit_len ? limit_len : length; b = size_a < size_b ? size_b - size_a : 0;
I dislike this usage. The code would be clearer if it said exactly what it is trying to do.
a = min( length, limit_len ); b = max( size_b - size_a, 0 );
/It is important to note that in C++ that second expression isn’t always correct. Since the unsigned ‘size_t’ type is used everywhere you can’t safely do the subtraction. This is a common, and well known, danger of using unsigned values for scalar values./
Avoid calculation errors
In an error-correcting library I’m using I found several statements like the below. Instead of performing a calculation which is undefined we substitute a default value.
return a == 0 ? 0 : c / a;
This code confuses me. Are we secretly hiding an error condition, or are we properly handling the error? It seems strange that though the limit as a approach 0 is infinity we substitute zero instead.
Null checks / optionals
A relatively common use is to extract properties from an object which may not actually be defined. This also includes using placeholder objects when the input is null.
bool set = object ? object->has_property() : false; process( opt_object ? opt_object : empty_object );
Unlike the previous calculation errors example, I find these relatively clear: the default values in this situation are clearly suitable as a result. Given that some such statements are clear, and others not, we could question the overall clarity of this form. Nonetheless, there needs to be some way to deal with optionals. A different syntax might be clearer, and is already available in some languages.
process( opt_object || empty_object );
There is also an ‘&&’ syntax for the first one, but I find it not as clear. It also only works since the default value is ‘false’.
A critical aspect to the null checks is that we expect the conditional operator to be lazy. Only the value which is actually needed should be evaluated. This makes the conditional operator distinct from a function call; in a function all parameters will always be evaluated, which would fail on a null object. You may also rely on this property to avoid potentially costly evaluations.
The operator is often used to fill in for a missing switch evaluation. This is easily served with a native switch evaluation.
q = item == const_a ? value_a :
item == const_b ? value_b :
item == const_c ? value_c :
In Leaf notation:
q = select [ item == const_a ? value_a, item == const_b ? value_b, item == const_c ? value_c, else value_else ]
Booleans are a fairly common part of programming. It is completely understandable that one needs to use them to select values. The ternary conditional is tailor made for this purpose.
a = value + ( is_high ? high_const : low_const); b = a + (is_high ? 10 : 1);
In isolation there is no way around such code. However, they are still a branch, and branches make your code more complex and error prone. The moment the same variable is checked multiple times it makes sense to isolate the check and provide a map instead.
consts = is_high ? high_consts : low_consts; a = value + consts.a_val; b = a + consts.b_val;
Expressions without conditionals in them are generally easier to read. It also allows the same style of coding to be used for ternary, and higher arity, toggles.
It all comes down to how often those boolean toggles are really needed. After replacing all those other cases with a better syntax I’m not sure if enough still remain to justify a special operator. If you have other scenarios you’d like me to consider then please tell me. I’m looking for feedback on how necessary this short-cut operator actually is.