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:
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:
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:
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:
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:
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:
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:
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:
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:
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
:
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:
if (window != null) {
window.close();
}
Inexperienced developers will sometimes use a try-catch block instead:
// 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:
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:
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:
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:
let name;
if (username) {
name = username;
} else {
name = 'friend';
}
console.log(`Hello, ${name}!`);
A conditional expression would be less noisy:
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:
let name = username ?? 'friend';
console.log(`Hello, ${name}!`);
Kotlin uses the symbol ?:
for its null coalescing operator:
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.