Leaf

Do we need the conditional ternary operator ?:

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.

Constraints

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.

Switches

The operator is often used to fill in for a missing switch evaluation. This is easily served with a native switch evaluation.

[sourecode]
q = item == const_a ? value_a :
item == const_b ? value_b :
item == const_c ? value_c :
value_else;
[/sourcecode]

In Leaf notation:

q = select [
	item == const_a ? value_a,
	item == const_b ? value_b,
	item == const_c ? value_c,
	else value_else
]

Boolean toggles

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.

Necessary convenience

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.

10 replies »

  1. From coffeescript i’ve got some idea’s like

    speed ?= 15
    equals

    if (speed == null) {
    speed = 15;
    }

    Some other idea’s

    speed = b?
    ape = b?2
    equals

    speed = (b != null) ? b : 0;
    ape = (b != null) ? b : 2;

    Maybe some things like:

    a = b || 4
    equals

    if(b == null)
    a = 4;
    else
    a =b

    also the inline if is realy neat but

    a = b ==3 ? b : 4;

    • Yes, Leaf has the ?= operator for default assign. I find it essential for working with optionals. I will likely also add the || syntax to take the object or a default if null. I’ll probably do something like ?| instead though, to make the syntax clearer.

      Part of the problem is that I use ‘:’ as a type separator and it is a really nice operator for that purpose. So if I retain the ?: ternary conditional I’d need some other symbols.

  2. Interesting article, your thoughts seem to evolve, it’s fascinating to observe that!

    Anyways, from C++ we have the std::iostream operators <>.
    It’s quite useful to use the ternary operator to avoid specifying the object for a second time:

    std::cout << (boolean ? "const char *one": "const char *two") << "Another const char *" << std::endl;

    instead of:

    if (boolean)
    std::cout << "const char *one";
    else
    std::cout << "const char *two";
    std::cout << "Another const char *" << std::endl;

    I believe this is a useful case of the ternary operator. But I actually use something else than both of the above…:

    bool boolean(true);
    std::cout < std::string {if (boolean) return “const char *one”; else return “const char *two”;})() < std::string can be omitted.

    If I need to expand my function a bit, or make it more readable:

    std::cout
    < std::string
    {
    if (boolean)
    return “const char *one”;
    else
    return “const char *two”;
    }
    )()
    << std::endl;

    Normally, I allow it to be up to 10 statements long. After this limit, the function has trouble self-documenting itself from the code alone, so I put it in a function instead.

    That's all I had to say, now I have a question:

    In leaf, you have "select []", and it seems to me that it's implemented as a if-else table with automatic returns combined by a lambda that calls this if-else table. Are other conditional operators allowed in this table?

    • I think my thoughts have evolved even more now and I’ll likely get to a completely different solution, which just happens to retain a ternary-like operation. Your examples are the ones that would make me most uncomfortable not having such a syntax: it’s just too common to fully ignore.

      The “table” is actually just a tuple value filled with “conditional” expressions. These are full expressions, so anything which evaluates to a boolean value is allowed. It has the same expressive power as a C++ “if” statement.

    • Somehow the code is really messed up, sorry about that. I guess it’s my bread ‘n water internet connection…

    • >Interesting article, your thoughts seem to evolve, it’s fascinating to observe that!

      Well they certainly aren’t intelligently designed =D Sorry, couldn’t resist ;)

  3. Check the haskell if xxx then … else … clauses. I think they look way less cryptic than the C equivalent. To unify things here, you can consider making if() return a value. However, if there is no variable getting the value, then the “then” and “else” types won’t need not match up

  4. I think a common way to use the ternary operator in languages like C++ is to keep variables const. For example:

    const int a = big ? 1000 : 1;

    This cannot be easily done, other than by creating a separate function returning 1000 or 1. Using an if statement forces a to be non-const.

    • This is a good obersvation for C++. In Leaf the unified conditional allows return values so this remains possible. The conditional will also resolve to a constant if all conditions are constant (allowing compile-time switches to work without macros).

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s