Local Definitions: let and where

There are two ways of declaring local definitions in Agda:

  • let-expressions

  • where-blocks

let-expressions

A let-expression defines an abbreviation. In other words, the expression that we define in a let-expression can neither be recursive, nor can let bound functions be defined by pattern matching.

Example:

  1. f : Nat
  2. f = let h : Nat Nat
  3. h m = suc (suc m)
  4. in h zero + h (suc zero)

let-expressions have the general form

  1. let f : A₁₁ A₁ₙ A
  2. f x x = e
  3. f : Aₘ₁ Aₘₖ A
  4. f x x = e
  5. in e

where previous definitions are in scope in later definitions. The type signatures can be left out if Agda can infer them. After type-checking, the meaning of this is simply the substitution e’[f₁ := λ x₁ … xₙ → e; …; fₘ := λ x₁ … xₖ → eₘ]. Since Agda substitutes away let-bindings, they do not show up in terms Agda prints, nor in the goal display in interactive mode.

Let binding record patterns

For a record

  1. record R : Set where
  2. constructor c
  3. field
  4. f : X
  5. g : Y
  6. h : Z

a let expression of the form

  1. let (c x y z) = t
  2. in u

will be translated internally to as

  1. let x = f t
  2. y = g t
  3. z = h t
  4. in u

This is not allowed if R is declared coinductive.

where-blocks

where-blocks are much more powerful than let-expressions, as they support arbitrary local definitions. A where can be attached to any function clause.

where-blocks have the general form

  1. clause
  2. where
  3. decls

or

  1. clause
  2. module M where
  3. decls

A simple instance is

  1. g ps = e
  2. where
  3. f : A A A
  4. f p₁₁ p₁ₙ= e
  5. f pₘ₁ pₘₙ= e

Here, the pᵢⱼ are patterns of the corresponding types and eᵢ is an expression that can contain occurrences of f. Functions defined with a where-expression must follow the rules for general definitions by pattern matching.

Example:

  1. reverse : {A : Set} List A List A
  2. reverse {A} xs = rev-append xs []
  3. where
  4. rev-append : List A List A List A
  5. rev-append [] ys = ys
  6. rev-append (x xs) ys = rev-append xs (x ys)

Variable scope

The pattern variables of the parent clause of the where-block are in scope; in the previous example, these are A and xs. The variables bound by the type signature of the parent clause are not in scope. This is why we added the hidden binder {A}.

Scope of the local declarations

The where-definitions are not visible outside of the clause that owns these definitions (the parent clause). If the where-block is given a name (form module M where), then the definitions are available as qualified by M, since module M is visible even outside of the parent clause. The special form of an anonymous module (module _ where) makes the definitions visible outside of the parent clause without qualification.

If the parent function of a named where-block (form module M where) is private, then module M is also private. However, the declarations inside M are not private unless declared so explicitly. Thus, the following example scope checks fine:

  1. module Parent where
  2. private
  3. parent = local
  4. module Private where
  5. local = Set
  6. module Public = Private
  7. test = Parent₁.Public.local

Likewise, a private declaration for a parent function does not affect the privacy of local functions defined under a module _ where-block:

  1. module Parent where
  2. private
  3. parent = local
  4. module _ where
  5. local = Set
  6. test = Parent₂.local

They can be declared private explicitly, though:

  1. module Parent where
  2. parent = local
  3. module _ where
  4. private
  5. local = Set

Now, Parent₃.local is not in scope.

A private declaration for the parent of an ordinary where-block has no effect on the local definitions, of course. They are not even in scope.

Proving properties

Sometimes one needs to refer to local definitions in proofs about the parent function. In this case, the module ⋯ where variant is preferable.

  1. reverse : {A : Set} List A List A
  2. reverse {A} xs = rev-append xs []
  3. module Rev where
  4. rev-append : List A List A List A
  5. rev-append [] ys = ys
  6. rev-append (x :: xs) ys = rev-append xs (x :: ys)

This gives us access to the local function as

  1. Rev.rev-append : {A : Set} (xs : List A) List A List A List A

Alternatively, we can define local functions as private to the module we are working in; hence, they will not be visible in any module that imports this module but it will allow us to prove some properties about them.

  1. private
  2. rev-append : {A : Set} List A List A List A
  3. rev-append [] ys = ys
  4. rev-append (x xs) ys = rev-append xs (x ys)
  5. reverse' : {A : Set} → List A → List A
  6. reverse' xs = rev-append xs []

More Examples (for Beginners)

Using a let-expression:

  1. tw-map : {A : Set} List A List (List A)
  2. tw-map {A} xs = let twice : List A List A
  3. twice xs = xs ++ xs
  4. in map (\ x twice [ x ]) xs

Same definition but with less type information:

  1. tw-map' : {A : Set} → List A → List (List A)
  2. tw-map' {A} xs = let twice : _
  3. twice xs = xs ++ xs
  4. in map (\ x twice [ x ]) xs

Same definition but with a where-expression

  1. tw-map'' : {A : Set} List A List (List A)
  2. tw-map'' {A} xs = map (\ x twice [ x ]) xs
  3. where twice : List A List A
  4. twice xs = xs ++ xs

Even less type information using let:

  1. g : Nat List Nat
  2. g zero = [ zero ]
  3. g (suc n) = let sing = [ suc n ]
  4. in sing ++ g n

Same definition using where:

  1. g' : Nat → List Nat
  2. g' zero = [ zero ]
  3. g' (suc n) = sing ++ g' n
  4. where sing = [ suc n ]

More than one definition in a let:

  1. h : Nat Nat
  2. h n = let add2 : Nat
  3. add2 = suc (suc n)
  4. twice : Nat Nat
  5. twice m = m * m
  6. in twice add2

More than one definition in a where:

  1. fibfact : Nat Nat
  2. fibfact n = fib n + fact n
  3. where fib : Nat Nat
  4. fib zero = suc zero
  5. fib (suc zero) = suc zero
  6. fib (suc (suc n)) = fib (suc n) + fib n
  7. fact : Nat Nat
  8. fact zero = suc zero
  9. fact (suc n) = suc n * fact n

Combining let and where:

  1. k : Nat Nat
  2. k n = let aux : Nat Nat
  3. aux m = pred (h m) + fibfact m
  4. in aux (pred n)
  5. where pred : Nat Nat
  6. pred zero = zero
  7. pred (suc m) = m