Skip to content

Commit 0f98b75

Browse files
authored
leap: add approaches (#1139)
1 parent 184f2f4 commit 0f98b75

File tree

8 files changed

+585
-0
lines changed

8 files changed

+585
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Conditional expression
2+
3+
```haskell
4+
isLeapYear :: Integer -> Bool
5+
isLeapYear year =
6+
if divisibleBy 100
7+
then divisibleBy 400
8+
else divisibleBy 4
9+
where
10+
divisibleBy d = year `mod` d == 0
11+
```
12+
13+
14+
## Conditional expressions
15+
16+
A _conditional expression_ (`if … then … else …`) is a compound expression that uses a test to determine which of two alternatives to evaluate to.
17+
Many other languages feature a similar construct, often termed 'ternary operator'.
18+
They are also known as _`if` expressions_.
19+
20+
When `p` is some expression of type `Bool` and `t` and `f` are any two expressions of the same type, then `if p then t else f` will
21+
22+
- evaluate to `t` if `p` evaluates to `True`, and
23+
- evaluate to `f` if `p` evaluates to `False`.
24+
25+
~~~~exercism/note
26+
Conditional expressions are [syntactic sugar][wikipedia-syntactic-sugar] for certain `case` expressions:
27+
28+
```haskell
29+
_ = if p then t else f
30+
-- is an abbreviation of
31+
_ = case p of
32+
True -> t
33+
False -> f
34+
```
35+
~~~~
36+
37+
38+
## In this approach
39+
40+
This approach uses exactly two tests to determine whether a year is a leap year.
41+
42+
The first test is for divisibility by 100.
43+
Once we know if the year is a multiple of 100, we know which further test to perform.
44+
45+
- If the year is evenly divisible by 100, then `divisibleBy 100` will evaluate to `True` and the entire `if` expression will evaluate to whatever `divisibleBy 400` evaluates to.
46+
- If the year is _not_ evenly divisible by 100, then `divisibleBy 100` is `False` and so the `if` expression evaluates to `divisibleBy 4`.
47+
48+
49+
## When to use `if`?
50+
51+
`if` expressions might be a good fit when you
52+
53+
- need an expression that
54+
- chooses between exactly two options
55+
- depending on a single `Boolean`.
56+
57+
When you have something other than a `Boolean`, use `case` instead.
58+
59+
When you do not strictly need an expression, an alternative is to [use guards][guards].
60+
61+
When you need to choose between more than two options, guards might be the solution.
62+
However, guards are not expressions and so are not always applicable.
63+
In such cases you might want to break out a multi-way `if`, available through the [`MultiWayIf` language extension][multiwayif-extension]:
64+
65+
```haskell
66+
{- LANGUAGE MultiWayIf -} -- at the top of the file
67+
68+
_ = if | condition -> expression
69+
| proposition -> branch
70+
| otherwise -> alternative
71+
-- which is syntactic sugar for
72+
_ = case () of
73+
_ | condition -> expression
74+
_ | proposition -> branch
75+
_ | otherwise -> alternative
76+
```
77+
78+
For more on this question, see [Guards vs. if-then-else vs. cases in Haskell][so-guards-if-cases] on StackOverflow.
79+
80+
81+
## An example of lazy evaluation
82+
83+
Just like 'ternary operators' in other languages, conditional expressions evaluate lazily.
84+
Specifically, only the relevant branch is evaluated:
85+
86+
```haskell
87+
ghci> error "Crash!" -- for demonstration
88+
*** Exception: Crash!
89+
ghci> if even 42 then "Success!" else error "Crash!"
90+
"Success!"
91+
```
92+
93+
Notice how evaluating the entire `if` expression does not result in a crash, even though one of its branches would if it were evaluated.
94+
95+
In our solution above we have
96+
97+
| year | `divisibleBy 100` | `divisibleBy 400` | `divisibleBy 4` | is leap year |
98+
| ---- | ----------------- | ----------------- | --------------- | ------------ |
99+
| 2020 | `False` | not evaluated | `True` | `True` |
100+
| 2019 | `False` | not evaluated | `False` | `False` |
101+
| 2000 | `True` | `True` | not evaluated | `True` |
102+
| 1900 | `True` | `False` | not evaluated | `False` |
103+
104+
105+
[guards]:
106+
https://exercism.org/tracks/haskell/exercises/leap/approaches/guards
107+
"Approach: a sequence of guards"
108+
109+
110+
[multiwayif-extension]:
111+
https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/multiway_if.html
112+
"GHC Users Guide: Multi-way if-expressions"
113+
[so-guards-if-cases]:
114+
https://stackoverflow.com/questions/9345589/
115+
"StackOverflow: Guards vs. if-then-else vs. cases in Haskell"
116+
[wikipedia-syntactic-sugar]:
117+
https://en.wikipedia.org/wiki/Syntactic_sugar
118+
"Wikipedia: Syntactic sugar"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
isLeapYear :: Integer -> Bool
2+
isLeapYear year =
3+
if divisibleBy 100
4+
then divisibleBy 400
5+
else divisibleBy 4
6+
where
7+
divisibleBy d = year `mod` d == 0
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"introduction": {
3+
"authors": [
4+
"MatthijsBlom"
5+
]
6+
},
7+
"approaches": [
8+
{
9+
"uuid": "a89d3a43-affc-4dc7-97b7-d5ce9353a8d5",
10+
"slug": "logical-expression",
11+
"title": "Logical expression",
12+
"blurb": "Use logical operators to combine several tests into one.",
13+
"authors": [
14+
"MatthijsBlom"
15+
]
16+
},
17+
{
18+
"uuid": "a6fbc4d9-911b-42cc-aa88-e015dd1f03b0",
19+
"slug": "guards",
20+
"title": "Guards",
21+
"blurb": "Use a sequence of guards.",
22+
"authors": [
23+
"MatthijsBlom"
24+
]
25+
},
26+
{
27+
"uuid": "708be8e8-0a2e-437d-8b39-6df37326890f",
28+
"slug": "conditional-expression",
29+
"title": "Conditional expression",
30+
"blurb": "Use a single conditional expression.",
31+
"authors": [
32+
"MatthijsBlom"
33+
]
34+
}
35+
]
36+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
isLeapYear :: Integer -> Bool
2+
isLeapYear year
3+
| indivisibleBy 4 = False
4+
| indivisibleBy 100 = True
5+
| indivisibleBy 400 = False
6+
| otherwise = True
7+
where
8+
indivisibleBy d = year `mod` d /= 0

0 commit comments

Comments
 (0)