|
| 1 | +# Guards |
| 2 | + |
| 3 | +```haskell |
| 4 | +isLeapYear :: Integer -> Bool |
| 5 | +isLeapYear year |
| 6 | + | indivisibleBy 4 = False |
| 7 | + | indivisibleBy 100 = True |
| 8 | + | indivisibleBy 400 = False |
| 9 | + | otherwise = True |
| 10 | + where |
| 11 | + indivisibleBy d = year `mod` d /= 0 |
| 12 | +``` |
| 13 | + |
| 14 | +## Guards |
| 15 | + |
| 16 | +Guards can optionally be added to patterns to constrain when they should match. |
| 17 | +For example, in |
| 18 | + |
| 19 | +```haskell |
| 20 | +ageCategory = case age of |
| 21 | + Just n | n >= 24 -> "Adult" |
| 22 | + Just n -> "Nonadult" |
| 23 | + Nothing -> "Eternal" |
| 24 | +``` |
| 25 | + |
| 26 | +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. |
| 27 | +Because patterns are checked in order, here |
| 28 | + |
| 29 | +- `Just 39` will match the first pattern and so result in `"Adult"`, but |
| 30 | +- `Just 5` will fall through to be matched to the second pattern, which will match, resulting in `"Nonadult"`. |
| 31 | + |
| 32 | +Patterns may contain multiple guards in sequence. |
| 33 | +These will then be checked in order just like patterns. |
| 34 | +The following variant on the above example produces exactly the same result. |
| 35 | + |
| 36 | +```haskell |
| 37 | +ageCategory = case age of |
| 38 | + Just n | n >= 24 -> "Adult" |
| 39 | + | otherwise -> "Nonadult" |
| 40 | + Nothing -> "Eternal" |
| 41 | +``` |
| 42 | + |
| 43 | +Here there is one fewer pattern, but the first one contains one more guard. |
| 44 | +`otherwise` is a synonym of `True`: it is the guard that always succeeds. |
| 45 | + |
| 46 | +Sequences of guards are analogous to `if`–`else if` chains in other languages. |
| 47 | + |
| 48 | + |
| 49 | +## In this approach |
| 50 | + |
| 51 | +When there are not many cases to match against, it is common to use _function definition [syntactic sugar][wikipedia-syntactic-sugar]_ instead of `case` because sometimes that is a bit nicer to read. |
| 52 | + |
| 53 | +```haskell |
| 54 | +categorize (Just n) |
| 55 | + | n >= 24 = "Adult" |
| 56 | + | otherwise = "Nonadult" |
| 57 | +categorize Nothing = "Eternal" |
| 58 | +-- is equivalent to / an abbreviation of |
| 59 | +categorize age = case age of |
| 60 | + Just n | n >= 24 -> "Adult" |
| 61 | + | otherwise -> "Nonadult" |
| 62 | + Nothing -> "Eternal" |
| 63 | +``` |
| 64 | + |
| 65 | +In the case of Leap, there aren't any interesting patterns to match against, so we only match against a name. |
| 66 | + |
| 67 | +```haskell |
| 68 | +-- A "binding pattern", just like `n` above. |
| 69 | +-- 👇 |
| 70 | +isLeapYear year |
| 71 | + | ... |
| 72 | +``` |
| 73 | + |
| 74 | +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. |
| 75 | + |
| 76 | +- If the year is not divisible by 4, then it is _certainly not_ a leap year. |
| 77 | +- If it is, then it _might_ be a leap year. |
| 78 | + - If divisible by 4 but not by 100, then it _certainly is_ a leap year. |
| 79 | + - If also divisible by 100, then it _might_ be a leap year. |
| 80 | + - If divisible by 4 and 100 but not by 400, then it is _certainly not_ a leap year. |
| 81 | + - Otherwise, i.e. if also divisible by 400, then it _certainly is_ a leap year. |
| 82 | + |
| 83 | +We can encode this sequence of checks using guards as follows. |
| 84 | + |
| 85 | +```haskell |
| 86 | +isLeapYear year |
| 87 | + | indivisibleBy 4 = False |
| 88 | + | indivisibleBy 100 = True |
| 89 | + | indivisibleBy 400 = False |
| 90 | + | otherwise = True |
| 91 | + where |
| 92 | + indivisibleBy d = year `mod` d /= 0 |
| 93 | +``` |
| 94 | + |
| 95 | +We need not start checking for divisibility by 4 specifically. |
| 96 | +Starting with 400 is also possible, but our checks and outcomes will be flipped: |
| 97 | + |
| 98 | +```haskell |
| 99 | +isLeapYear :: Integer -> Bool |
| 100 | +isLeapYear year |
| 101 | + | divisibleBy 400 = True |
| 102 | + | divisibleBy 100 = False |
| 103 | + | divisibleBy 4 = True |
| 104 | + | otherwise = False |
| 105 | + where |
| 106 | + divisibleBy d = year `mod` d == 0 |
| 107 | +``` |
| 108 | + |
| 109 | +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. |
| 110 | +Using guards is still possible, but it necessarily looks different: |
| 111 | + |
| 112 | +```haskell |
| 113 | +isLeapYear year |
| 114 | + | divisibleBy 100 = divisibleBy 400 |
| 115 | + | otherwise = divisibleBy 4 |
| 116 | + where |
| 117 | + divisibleBy d = year `mod` d == 0 |
| 118 | +``` |
| 119 | + |
| 120 | +This is very similar to the [conditional expression approach][conditional-expression]. |
| 121 | + |
| 122 | + |
| 123 | + |
| 124 | +## When to use guards? |
| 125 | + |
| 126 | +Many beginning Haskellers write code like |
| 127 | + |
| 128 | +```haskell |
| 129 | +fromMaybe :: a -> Maybe a -> a |
| 130 | +fromMaybe x m |
| 131 | + | isJust m = fromJust m |
| 132 | + | otherwise = x |
| 133 | +``` |
| 134 | + |
| 135 | +or |
| 136 | + |
| 137 | +```haskell |
| 138 | +fromMaybe :: a -> Maybe a -> a |
| 139 | +fromMaybe x m |
| 140 | + | m == Nothing = x |
| 141 | + | otherwise = fromJust m |
| 142 | +``` |
| 143 | + |
| 144 | +Don't do this. |
| 145 | +Use `case` instead, whenever possible. |
| 146 | +The compiler will be much more able to help you if you do, such as by checking that you have covered all possible cases. |
| 147 | +It is also nicer to read. |
| 148 | + |
| 149 | +Use guards |
| 150 | + |
| 151 | +- to narrow down when patterns should match, or |
| 152 | +- in lieu of other languages' `if`–`else if` chains. |
| 153 | + |
| 154 | +Because guards are not themselves expressions, the latter use is not always possible. |
| 155 | +In such cases, the [`MultiWayIf` language extension][multiwayif-extension] has your back: |
| 156 | + |
| 157 | +```haskell |
| 158 | +{- LANGUAGE MultiWayIf -} -- at the top of the file |
| 159 | + |
| 160 | +_ = if | condition -> expression |
| 161 | + | proposition -> branch |
| 162 | + | otherwise -> alternative |
| 163 | +-- which is syntactic sugar for |
| 164 | +_ = case () of |
| 165 | + _ | condition -> expression |
| 166 | + _ | proposition -> branch |
| 167 | + _ | otherwise -> alternative |
| 168 | +``` |
| 169 | + |
| 170 | +For more on this question, see [Guards vs. if-then-else vs. cases in Haskell][so-guards-if-cases] on StackOverflow. |
| 171 | + |
| 172 | + |
| 173 | +[conditional-expression]: |
| 174 | + https://exercism.org/tracks/haskell/exercises/leap/approaches/conditional-expression |
| 175 | + "Approach: a conditional expression" |
| 176 | + |
| 177 | + |
| 178 | +[multiwayif-extension]: |
| 179 | + https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/multiway_if.html |
| 180 | + "GHC Users Guide: Multi-way if-expressions" |
| 181 | +[so-guards-if-cases]: |
| 182 | + https://stackoverflow.com/questions/9345589/ |
| 183 | + "StackOverflow: Guards vs. if-then-else vs. cases in Haskell" |
| 184 | +[wikipedia-syntactic-sugar]: |
| 185 | + https://en.wikipedia.org/wiki/Syntactic_sugar |
| 186 | + "Wikipedia: Syntactic sugar" |
0 commit comments