Haskell from the ground up

Guards and readable choices

Yes and no values, comparisons, and the two ways to choose an answer: a chain of guards or a single if expression.

Part 4 of 12

Beginner14 min readBack to Courses

Yes and no values

Bool has exactly two values, True and False. Comparisons produce them: 95 > 70 is True, and 95 < 70 is False. Equality is written == and inequality is written /=, two similar-looking signs with opposite meanings, so read them carefully.

The operators && and || join two conditions, and not flips one. A guard or an if needs a Bool, and the compiler will say so if you hand it a number instead.

A chain of guards

A guard is a condition written beside the answer it selects. The compiler tries the guards from the top and takes the first one that is True, so the order you write them in is the order of the decisions.

The last guard is usually otherwise, which is simply True under another name, so it catches everything that got past the tests above it. Writing otherwise is the habit that stops a chain from having no answer at all.

if is an expression

if is not a statement in this language, it is an expression, so it has to produce a value in both cases. That is why the else branch cannot be left out: without it, the expression would have no value when the condition is false.

Two outcomes read well as an if. Once there are three or more, a chain of guards is usually easier to read, because each condition sits next to the answer it chooses.

Naming a condition with where

A guard can hold a whole expression, and a long one makes the chain hard to scan. where attaches names to the equation above, so a test can be written once and used in several guards. adult age = age >= 18 is a one-line example; inside a larger function, where grown = age >= 18 lets the guards read | grown = ....

The names in a where belong to that equation only, and they can see its arguments. That is why they are a good place for a threshold or a derived value that several guards share. Keeping the arithmetic out of the guard line leaves the conditions visible, and a condition you can read at a glance is the whole reason for using guards rather than nested ifs.

A choice with two branches

sign :: Int -> String
sign n = if n == 0 then "zero" else "not zero"

absolute :: Int -> Int
absolute n
  | n < 0 = 0 - n
  | otherwise = n

Gold, silver, bronze

Run it, then add a guard for a score of 0 that answers with a sentence of its own, and check that the order you put it in still makes sense.

Output
Click "Run" to execute your code

A chain of guards without otherwise can leave a value with no answer, and then the program stops when that value finally arrives. Order matters too: putting the test for 70 above the test for 90 would make every high score come back silver.

Comparisons answer Bool, and Bool is all a guard reads

Every comparison is a function that answers a Bool, which is why a guard can be any expression of that type. 95 > 70 is True, not (95 > 70) is False, and 95 > 70 && 12 > 50 is False because the second half fails.

There is no chained comparison here. 0 < n < 10 is read as (0 < n) < 10, and the compiler then complains that a Bool cannot be compared with a number. Write 0 < n && n < 10: two tests joined by a condition.

&& and || are lazy in their second argument. If the left side already decides the answer, the right side is never evaluated, which is why not (null xs) && head xs > 0 is safe while the same two tests in the other order would stop on an empty list.

Both branches must answer the same type

An if is an expression, so the then and the else branches have to produce the same type. One branch answering text and the other a number is not a value with two moods; it is a program the compiler refuses.

A guard chain follows the same rule: every guarded answer has to agree with the type line. The conditions beside the answers can be as different as you like, but the answers themselves have to be interchangeable.

That constraint is what makes a function safe to call. Whatever the arguments, the caller knows the type of what comes back, so a branch cannot hand back a surprise.

= defines and == compares, so if n = 0 is a syntax error rather than a test. 0 < n < 10 does not mean a range, because the first comparison already produced a Bool. And not n > 5 groups as (not n) > 5, which fails to type-check, since not applies to a Bool rather than a number; write not (n > 5).

Try these next

  • Add a guard for a score of exactly 100 that answers with its own word, and decide where it has to sit.
  • Write between low high n with two comparisons joined by &&, and test both ends of the range.
  • Rewrite evenOrOdd with guards instead of an if, then rewrite grade with nested ifs and see which reads better.

Postage bands and a two-part test

Run it and read the six lines. Then add a guard that calls anything over 2000 grams freight, and work out the rate line for 2500 before you run it.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

For grade 95, which guard decides the answer?