Profile Lucas Sta Maria

I'm a final year Computer Science and Mathematics student at Northeastern University interested in programming languages and developer tooling.

(λx.x x) (λx.x x)

A Take: The Ternary Operator is Nice

People often consider the ternary operator to lend itself to ugly-looking code, but I find that–in certain scenarios–it's incredibly idiomatic. What we can fail to see is that the ternary operator is not a direct substitute for if/else branches in imperative languages – they have different semantics and thus serve different purposes. Perhaps the greatest difference is that, in Java, if/else branches are statements – they do not return a value. Thus, the blocks contained inside expect imperative statements. Ternary operators, on the other hand, expect expressions and return an expression.

Here's where we can expect if/else statements to be used:

public enum Drink {
    Water,
    Milk,
}

public class Example {
    public static void main(String[] args) {
        Drink drink = Something.getRandomDrink();
        
        // Note this can be substituted for switch/case statement.
        if (drink == Drink.Water) {
            System.out.println("Water is cool");
        } else {
            System.out.println("Milk is white");
        }
    }
}

In the above code, the bodies of our if/else statements are also imperative statements with side-effects. What we can identify, however, is that we're really performing two steps here:

  1. Determining the string to that we want to print based on the value.
  2. Actually printing the string we get.

When we make that observation, we can transform our code into the following:

public enum Drink {
    Water,
    Milk,
}

public class Example {
    public static void main(String[] args) {
        Drink drink = Something.getRandomDrink();
        
        final String drinkDescriptor = 
            drink == Drink.Water ? "Water is cool" : "Milk is white";
            
        System.out.println(drinkDescriptor);
    }
}

I'd argue that this code is better, since it would allow us to use the result drinkDescriptor later on if we wanted. But it does seem somewhat limiting, doesn't it? What if our definition of the enum Drink had more than two members in it?

public enum Drink {
    Water,
    Milk,
    BubbleTea,
}

How are we supposed to use ternary operators for that? Java has switch-statements, which seem to be what we really want.

public class Example {
    public static void main(String[] args) {
        Drink drink = Something.getRandomDrink();
        
        String drinkDescriptor = null;
        switch (drink) {
            case Drink.Water:
                drinkDescriptor = "Water is cool";
                break;
            case Drink.Milk:
                drinkDescriptor = "Milk is white";
                break;
            case Drink.BubbleTea:
                drinkDescriptor = "Bubble tea is sweet";
        }
        
        System.out.println(drinkDescriptor);
    }
}

There are actually a few problems with switch-statements. The first is similar to if-statements: it's a lot cleaner to assign the value of drinkDescriptor directly to an expression rather than later through mutation. The second is case-fallthrough: what happens if we accidentally forget a break? Then our drinkDescriptor will contain an unintended value.

The clever solution to this requires realising that ternary operators are also chainable. By chaining them, we can alter our solution to:

public class Example {
    public static void main(String[] args) {
        Drink drink = Something.getRandomDrink();
        
        final String drinkDescriptor = 
            drink == Drink.Water ? "Water is cool" : 
            drink == Drink.Milk  ? "Milk is white" :
                                   "Bubble tea is sweet";
        
        System.out.println(drinkDescriptor);
    }
}

That way we assign our drinkDescriptor variable to an expression. Perhaps the most useful observation you can make is that these can be represented as simple S-expressions in Racket. The ternary operator is essentially a flavoured version of our if-expression from Racket.

;; A Drink is one of
;; - 'Water
;; - 'Milk

(define (describe-drink drink)
  (if (symbol=? drink 'Water)
    "Water is cool"
    "Milk is white"))

(displayln (describe-drink (get-random-drink)))

And even better, if we have multiple cases, our chained ternary operators just represent a cond:

;; A Drink is one of
;; - 'Water
;; - 'Milk
;; - 'BubbleTea

(define (describe-drink drink)
  (cond [(symbol=? drink 'Water)   "Water is cool"]
        [(symbol=? drink 'Milk)    "Milk is white"]
        [else                      "Bubble tea is sweet"]))

(displayln (describe-drink (get-random-drink)))

The ternary operator isn't just some keyboard-mushing, and it isn't an ugly substitute for if-statements. It offers different semantics and thus different use-cases, and it should be leveraged when necessary.