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 @try is 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 where binding nothing uses is never computed. A let binding nothing uses still runs where it is written.

  • In a do block, a let never 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.