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.
4.11.4. When bindings are evaluated
Morloc is strict: an argument is evaluated once, where it is applied, no
matter how many times the function uses it. Named values follow the same
rule. A where or let binding is computed at most once each time its
scope is evaluated, however many times it is mentioned:
-
A binding used in only one branch of a conditional is computed in that branch, so the other branch does not pay for it (or fail on it).
-
A binding used only inside
@tryis computed inside it, so its failure is caught there. -
A value used inside a lambda is computed once, when the lambda is built, not on every call.
-
A
wherebinding nothing uses is never computed. Aletbinding nothing uses still runs where it is written. -
In a
doblock, aletnever runs before the effects written above it. -
A top-level value is computed at most once per command.
A named value is also one value across languages: if Python and C++ code both read it, it is computed once and passed to both.