Conditional Expressions

Dear Computer

Chapter 6: Expressions

Conditional Expressions

Any interesting program makes choices. Imperative languages make choices about which statements to execute using if-else and switch structures. Functional languages make choices about which values to yield using conditional expressions.

N-Way Conditionals

We've briefly seen Haskell's if-else expression, but here it is again in the context of a function that turns a number of hours in 24-hour time to 12-hour time:

Haskell
twelveHourTime :: Int -> String
twelveHourTime hours =
  if hours == 0
    then "12 AM"
    else if 1 <= hours && hours <= 11
      then show hours ++ " AM"
      else if hours == 12
        then "12 PM"
        else if 13 <= hours && hours <= 23
          then show (mod hours 12) ++ " PM"
          else error "illegal number of hours"

twelveHourTime 0    -- yields "12 AM"
twelveHourTime 5    -- yields "5 AM"
twelveHourTime 12   -- yields "12 PM"
twelveHourTime 23   -- yields "11 PM"

The builtin show function called in this function turns its parameter into a string, much like toString in Java and to_s in Ruby. The name show is misleading. It is not a print function that produces a side effect. It returns a String.

The if-else structure is syntactically heavy when there are more than two choices. The secondary choices must be nested, and nested code is harder to read than unnested code. Imperative languages trick programmers into thinking they have a readable n-way branching structure in the if-else-if statement. There is no special if-else-if statement; it is just an illusion. Observe this implementation of twelveHourTime in Java that uses nested if-else statements:

Java
public static String twelveHourTime(int hours) {
  if (hours == 0) {
    return "12 AM";
  } else {
    if (1 <= hours && hours <= 11) {
      return String.format("%d AM", hours);
    } else {
      if (hours == 12) {
        return "12 PM";
      } else {
        if (13 <= hours && hours <= 23) {
          return String.format("%d PM", hours % 12);
        } else {
          throw new IllegalArgumentException("hours must be in [0, 23]");
        }
      }
    }
  }
}

When a block consists of only a single statement, it does not need to be enclosed in curly braces. Suppose we remove only the curly braces from the else-blocks that have an if statement nested within:

Java
public static String twelveHourTime(int hours) {
  if (hours == 0) {
    return "12 AM";
  } else
    if (1 <= hours && hours <= 11) {
      return String.format("%d AM", hours);
    } else 
      if (hours == 12) {
        return "12 PM";
      } else
        if (13 <= hours && hours <= 23) {
          return String.format("%d PM", hours % 12);
        } else {
          throw new IllegalArgumentException("hours must be in [0, 23]");
        }



}

If we merge the else and succeeding if lines together and reindent, we end up with an n-way branching structure that more clearly reveals its ladder of choices:

Java
public static String twelveHourTime(int hours) {
  if (hours == 0) {
    return "12 AM";
  } else if (1 <= hours && hours <= 11) {
    return String.format("%d AM", hours);
  } else if (hours == 12) {
    return "12 PM";
  } else if (13 <= hours && hours <= 23) {
    return String.format("%d PM", hours % 12);
  } else {
    throw new IllegalArgumentException("hours must be in [0, 23]");
  }
}

This if-else-if statement is not a special syntactic form. It's just a collection of if-elses with some reformatting. Python and Ruby do have dedicated n-way branching structures. Secondary choices in these languages are marked with the words elif and elsif, respectively.

Haskell provides several other conditional expression structures that are compact and readable like the if-else-if statement. One is a function body defined using guards, which has this grammatical form:

Haskell
functionName param1 param2...
  | condition1 = value1
  | condition2 = value2
  | ...
  | otherwise = valueN

Each guard is a boolean predicate followed by an expression whose value is yielded when the predicate is true. The last predicate, otherwise, is a synonym for True. If all prior predicates are False, the last case will provide the fallback value or generate an error. Guards reduce twelveHourTime to much denser code:

Haskell
twelveHourTime :: Int -> String
twelveHourTime hours
  | hours == 0 = "12 AM"
  | 1 <= hours && hours <= 11 = show hours ++ " AM"
  | hours == 12 = "12 PM"
  | 13 <= hours && hours <= 23 = show (mod hours 12) ++ " PM"
  | otherwise = error "illegal hours"

Normally an equal sign (=) follows after a function header, but not in a function whose body is made of guards. There isn't one after the twelveHourTime header. Instead an equal sign appears after each predicate.

Guards ask a series of questions, whereas a case expression does a series of comparisons. It compares a single value against a set of possible matches and yields the value associated with the first successful match. This Haskell function classifies a pine tree according to the number of needles in a cluster or fascicle:

Haskell
pineKind :: Int -> String
pineKind needleCount =
  case needleCount of
    2 -> "red pine"
    3 -> "pitch pine"
    5 -> "white pine"
    _ -> error "unknown species"

The cases are checked in order. An underscore matches any value. It necessarily appears last.

The conditions of Haskell's case expression do not permit ranges like guards do and are not a good fit for writing twelveHourTime. Ruby has a case expression that is more accommodating:

Ruby
def twelve_hour_time(hours)
  case hours
    when 0
      "12 AM"
    when 1..11
      "#{hours} AM"
    when 12
      "12 PM"
    when 13..23
      "#{hours % 12} PM"
    else
      raise "hours must be in [0, 23]"
  end
end

Null Operators

Imagine we've got a program with this statement:

Java
window.close(); 

Even if the close method of the Window is very carefully written to avoid errors, this statement can still fail because an earlier statement might have nulled window:

Java
window = null;
// ...
window.close(); 

The safety of Window hardly matters when we don't have a Window at all. In any language where references can be assigned null, we run the risk of null pointer exceptions. One way to avoid them is with a preemptive conditional statement:

Java
if (window != null) {
  window.close(); 
}

Inexperienced developers will sometimes use a try-catch block instead:

Java
// BAD EXAMPLE
try {
  window.close(); 
} catch (NullPointerException e) {
}

Using a try-catch block here is a bit like calling the police after inviting a suspected burglar into our home. It's better to check if someone is a burglar before inviting them in. Likewise, we should favor preemptive conditional statements over reactive exception handling. However, these add a lot of clutter to the code, especially if we must check a chain of accesses for null:

Java
if (user != null) {
  if (user.options != null) {
    if (user.options.language != null) {
      user.options.language.switch();
    }
  }
}

Some languages, including JavaScript and C#, provide a null chaining or optional chaining operator that condenses this conditional logic down to a compact expression. A question mark is placed after a potentially null receiver:

JavaScript
window?.close();
user?.options?.language?.switch();

If the receiver before a question mark is null, the evaluation is abandoned so that no null pointer exception occurs, and null is yielded to the expression's surrounding context.

Ruby calls this the safe navigation operator and uses an ampersand instead of a question mark:

Ruby
window&.close();
user&.options&.language&.switch();

In other situations, we don't want to just abandon the expression if we hit a null. We want to produce a default value in its place. We could use a conditional statement to navigate this choice:

JavaScript
let name;
if (username) {
  name = username;
} else {
  name = 'friend';
}

console.log(`Hello, ${name}!`);

A conditional expression would be less noisy:

JavaScript
let name = username ? username : 'friend';
console.log(`Hello, ${name}!`);

However, the logic of choosing between a nullable value and a default value is exactly the job of the null coalescing operator, as found in C# and JavaScript. The two values are separated with ??. If the left operand is not null, its value is yielded. If it is null, the value of the right operand is yielded. This shortens the code considerably:

JavaScript
let name = username ?? 'friend';
console.log(`Hello, ${name}!`);

Kotlin uses the symbol ?: for its null coalescing operator:

Kotlin
name = username ?: 'friend'
puts "Hello, #{name}!"

It's sometimes called the Elvis operator because of the way the question mark curls down over the brow.

← OperatorsCustom Operators →