Section. Defunctional normalisation by evaluation [nbe-nf3]

Now let's move on to optimise wnf2 further. A notable source of inefficiency is the work that we need to do for shifting de Bruijn indices. De Bruijn indices are only for representing variables in a scope-safe way. They shall not play any central role in normalisation. Therefore we'd like to optimise them out.

Let's first work on the shifting in this line of wnf2:

       Closure2 b sub' -> shift0Val (-1) (wnf2 b (shift0List 1 (a' : sub')))

These two shifting operations have their origin from substitution, namely the following line from our earlier wnf1 (with beta inlined for easier comparison):

       Abs b -> wnf1 (shift 0 (-1) (subst b 0 (shift 0 1 a')))

An observation useful to us is that in the setting of wnf2 the lazy substitution a' : sub' substitutes all the free variables of b. Because weak normalisation on a term never introduces new free variables, we know for sure that the free variables in the term wnf2 b (shift0List 1 (a' : sub') must all come from free variables in the lazy substitution a' : sub'. Therefore we could do the final step shift0Val (-1) of shifting free variables by -1 before the wnf2 as well. That is to say, we have equality

   shift0Val (-1) (wnf2 b (shift0List 1 (a' : sub')))
=  wnf2 b (shift0List (-1) (shift0List 1 (a' : sub')))

Then shift0List (-1) (shift0List 1 ...) just cancels out!

   wnf2 b (shift0List (-1) (shift0List 1 (a' : sub')))
=  wnf2 b (a' : sub')

The observation allows us to optimise wnf2 into the following wnf3, resulting in a new normaliser nf3:

wnf3 :: Tm -> Subst2 -> Val2
wnf3 (Var n) sub = sub !! n
wnf3 (Abs b) sub = Closure2 b sub
wnf3 (App f a) sub =
  let f' = wnf3 f sub
      ~a' = wnf3 a sub
  in case f' of
       Closure2 b sub' -> wnf3 b (a' : sub')
       Spine2 v as -> Spine2 v (a' : as)

nf3 :: Tm -> Tm
nf3 t = let n = fv t
        in reify2 (wnf3 t (reflect2 n))

The same sanity checks again:

-- >>> nf3 c2
-- \x0. \x1. x0 (x0 x1)

-- >>> nf3 (cmul `App` c2 `App` (cinc `App` c2))
-- \x0. \x1. x0 (x0 (x0 (x0 (x0 (x0 x1)))))

-- >>> nf3 (cmul `App` c2 `App` (cinc `App` c2) `App` Var 0 `App` Var 1)
-- x1 (x1 (x1 (x1 (x1 (x1 x0)))))

-- >>> nf3 (cmul `App` (cinc `App` c2) `App` (cinc `App` c2))
-- \x0. \x1. x0 (x0 (x0 (x0 (x0 (x0 (x0 (x0 (x0 x1))))))))

The normaliser nf3 is essentially already normalisation by evaluation for untyped lambda calculus. To normalise a term t, we first evaluate it into a semantic domain Subst2 -> Val2 -- note the semantic domain is Subst2 -> Val2 rather than just Val2! From the resulting value, we obtain a normal form by reflecting the context of the term t into a substitution and then reifying the value.