Guards

Leap
Leap in Haskell
isLeapYear :: Integer -> Bool
isLeapYear year
  | indivisibleBy 4   = False
  | indivisibleBy 100 = True
  | indivisibleBy 400 = False
  | otherwise         = True
  where
    indivisibleBy d = year `mod` d /= 0

Guards

Guards can optionally be added to patterns to constrain when they should match. For example, in

ageCategory = case age of
  Just n | n >= 24 -> "Adult"
  Just n           -> "Nonadult"
  Nothing          -> "Eternal"

the pattern Just n will match both values Just 5 and Just 39, but the pattern Just n | n >= 24 will only match the latter. Because patterns are checked in order, here

  • Just 39 will match the first pattern and so result in "Adult", but
  • Just 5 will fall through to be matched to the second pattern, which will match, resulting in "Nonadult".

Patterns may contain multiple guards in sequence. These will then be checked in order just like patterns. The following variant on the above example produces exactly the same result.

ageCategory = case age of
  Just n | n >= 24   -> "Adult"
         | otherwise -> "Nonadult"
  Nothing            -> "Eternal"

Here there is one fewer pattern, but the first one contains one more guard. otherwise is a synonym of True: it is the guard that always succeeds.

Sequences of guards are analogous to if–else if chains in other languages.

In this approach

When there are not many cases to match against, it is common to use function definition syntactic sugar instead of case because sometimes that is a bit nicer to read.

categorize (Just n)
  | n >= 24        = "Adult"
  | otherwise      = "Nonadult"
categorize Nothing = "Eternal"
-- is equivalent to / an abbreviation of
categorize age = case age of
  Just n | n >= 24   -> "Adult"
         | otherwise -> "Nonadult"
  Nothing            -> "Eternal"

In the case of Leap, there aren't any interesting patterns to match against, so we only match against a name.

-- A "binding pattern", just like `n` above.
--          👇
isLeapYear year
  | ...

It turns out that, if we are careful to ask questions in the right order, we can always potentially attain certainty about the answer by asking one more question.

  • If the year is not divisible by 4, then it is certainly not a leap year.
  • If it is, then it might be a leap year.
    • If divisible by 4 but not by 100, then it certainly is a leap year.
    • If also divisible by 100, then it might be a leap year.
      • If divisible by 4 and 100 but not by 400, then it is certainly not a leap year.
      • Otherwise, i.e. if also divisible by 400, then it certainly is a leap year.

We can encode this sequence of checks using guards as follows.

isLeapYear year
  | indivisibleBy 4   = False
  | indivisibleBy 100 = True
  | indivisibleBy 400 = False
  | otherwise         = True
  where
    indivisibleBy d = year `mod` d /= 0

We need not start checking for divisibility by 4 specifically. Starting with 400 is also possible, but our checks and outcomes will be flipped:

isLeapYear :: Integer -> Bool
isLeapYear year
  | divisibleBy 400 = True
  | divisibleBy 100 = False
  | divisibleBy   4 = True
  | otherwise       = False
  where
    divisibleBy d = year `mod` d == 0

Starting with 100 is more complicated: both years divisible by 100 and years not divisible by 100 sometimes are and sometimes aren't leap years. Using guards is still possible, but it necessarily looks different:

isLeapYear year
  | divisibleBy 100 = divisibleBy 400
  | otherwise       = divisibleBy   4
  where
    divisibleBy d = year `mod` d == 0

This is very similar to the conditional expression approach.

When to use guards?

Many beginning Haskellers write code like

fromMaybe :: a -> Maybe a -> a
fromMaybe x m
  | isJust m  = fromJust m
  | otherwise = x

or

fromMaybe :: a -> Maybe a -> a
fromMaybe x m
  | m == Nothing = x
  | otherwise    = fromJust m

Don't do this. Use case instead, whenever possible. The compiler will be much more able to help you if you do, such as by checking that you have covered all possible cases. It is also nicer to read.

Use guards

  • to narrow down when patterns should match, or
  • in lieu of other languages' if–else if chains.

Because guards are not themselves expressions, the latter use is not always possible. In such cases, the MultiWayIf language extension has your back:

{- LANGUAGE MultiWayIf -}  -- at the top of the file

_ = if | condition   -> expression
       | proposition -> branch
       | otherwise   -> alternative
-- which is syntactic sugar for
_ = case () of
  _ | condition   -> expression
  _ | proposition -> branch
  _ | otherwise   -> alternative

For more on this question, see Guards vs. if-then-else vs. cases in Haskell on StackOverflow.

11th Sep 2024 · Found it useful?