4.11. where and let clauses
Both introduce local bindings, and they differ in exactly one way that matters:
where is order-invariant, let is sequential. Pick whichever fits how you
want to read the definition.
4.11.1. where
A where clause hangs local bindings off the end of a definition:
f1 :: Int -> Int
f1 x = y + b where
y = x + 1
b = 41
$ ./locals f1 1
43
Bindings in a where block are order-independent and may refer to each other
freely, though not mutually recursively. They can see the function’s arguments,
and the main expression can see them.
Clauses inherit their parent’s scope and nest:
f2 :: Int
f2 = x where
x = y where
y = a + b
a = 1
b = 41
$ ./locals f2
42
Note that the inner clause sees b from the outer one.
4.11.2. let
let is the more orderly cousin. Several bindings may precede the terminal
in, they run in order, and each may only refer to names bound above it:
f3 :: Int -> Int
f3 n =
let m = n + 1
y = m + 2
in (m + y)
$ ./locals f3 1
6
4.11.3. The scope rule that separates them
let is non-recursive sequential: each binding is in scope for everything
after it, and a later binding may shadow an earlier one of the same name. So a
chain of single-binding `let`s is legal, and the last one wins:
foo :: Int
foo = let x = 1
let x = 2
in x
$ ./locals foo
2
where is order-invariant: every binding sees every other one. That makes
shadowing meaningless, so a name may be bound only once in a clause, and it may
not collide with a function parameter. Both violations are compile-time errors.
Binding the same name twice:
whx.loc:8:3: duplicate binding in where-clause: y
|
8 | y = n + 2
| ^
Binding a name that is already a parameter:
whx.loc:7:3: where-clause binding shadows function parameter: x
|
7 | x = 100
| ^
If you want shadowing, that is what let is for.