chriswarbo-net: 0d60c333d9f6b78b3dc11d89f934c5400592edab

     1: ---
     2: title: Lazy Lambda Calculus
     3: ---
     4: 
     5: ```{pipe="cat >> 1.hs"}
     6: import Test.SmallCheck
     7: 
     8: ```
     9: 
    10: Recently I've been playing with meta-programming in [lambda calculus] [1] (originally I tried using [combinatory logic] [2] but the combinators rapidly became too large to understand :( ). For this, I wanted an LC implementation with the following characteristics:
    11: 
    12:  - Implemented in Haskell
    13:  - Terms which I can pattern-match against (ie. not just regular functions)
    14:  - [De Bruijn indices] [3]
    15:  - A non-diverging (ie. [co-recursive] [4]) evaluation function
    16: 
    17: [1]: http://en.wikipedia.org/wiki/Lambda_calculus
    18: [2]: http://en.wikipedia.org/wiki/Combinatory_logic
    19: [3]: http://en.wikipedia.org/wiki/De_Bruijn_index
    20: [4]: http://en.wikipedia.org/wiki/Corecursion
    21: 
    22: I tried implementing this myself, and here's the infrastructure I built to do it:
    23: 
    24: ```{.haskell pipe="tee -a 1.hs"}
    25: -- Peano-style natural numbers
    26: data Nat = Z
    27:          | S Nat
    28: 
    29: -- Co-inductive wrapper to allow undefined output (infinite Laters)
    30: data Partial a = Now a
    31:                | Later (Partial a)
    32: 
    33: -- We can map functions over partial results
    34: instance Functor Partial where
    35:   fmap f (Now   x) = Now        (f x)
    36:   fmap f (Later x) = Later (fmap f x)
    37: 
    38: -- Partiality is a monad
    39: instance Monad Partial where
    40:   return = Now  -- Immediate value
    41:   (Now   x) >>= f = Later (f x)
    42:   (Later x) >>= f = Later (x >>= f)
    43: 
    44: -- LC terms
    45: data Term a = Const a       -- Opaque Haskell values
    46:             | Var Nat       -- De Bruijn index
    47:             | Lam (Term a)  -- Anonymous function
    48:             | Term :@ Term  -- Function application
    49: 
    50: ```
    51: 
    52: My first attempt hit problems with [closure] [5]. Specifically, I was trying to make an evaluation function with Terms as input **and** output, but this gave me nowhere to store the associated environment:
    53: 
    54: [5]: http://en.wikipedia.org/wiki/Closure_%28computer_programming%29
    55: 
    56: ```{.haskell pipe="tee -a 1.hs"}
    57: -- (Broken) evaluation function
    58: eval' :: [Partial (Term a)] -> Term a -> Partial (Term a)
    59: eval' (e:es) (Var  Z)    = e
    60: eval' (e:es) (Var (S n)) = eval' es (Var n)
    61: eval'  e     (l :@ r)    = let ev = eval' e in
    62:                            do l' <- ev l
    63:                               case l' of
    64:                                 Lam f -> Later (eval' (ev r : e) f)
    65:                                 _     -> error "Can only apply Lams"
    66: eval'  e      t          = t
    67: 
    68: eval = eval' []
    69: 
    70: ```
    71: 
    72: To see why this is incorrect, we can use [SmallCheck] [6]. Let's check the property that closed terms (ie. those with no free variables) remain closed after evaluation:
    73: 
    74: [6]: http://hackage.haskell.org/package/smallcheck
    75: 
    76: ```haskell
    77: -- Predicate to see if a Term has fewer than n free variables
    78: closed' n (Var m)  = m < n
    79: closed' n (Lam f)  = closed' (S n) f
    80: closed' n (l :@ r) = closed' n l && closed' n r
    81: closed' _ _        = True
    82: 
    83: closed = closed' 0
    84: 
    85: -- Helper functions
    86: 
    87: -- Force a result in n steps, or else fail
    88: force :: Nat -> Partial a -> Maybe a
    89: force  Z     _        = Nothing
    90: force (S n) (Now   x) = Just x
    91: force (S n) (Later x) = force n x
    92: 
    93: -- A conservative decision procedure
    94: trueIn n x = case force n x of
    95:                Just b  -> b
    96:                Nothing -> False
    97: 
    98: -- A lax decision procedure
    99: notFalseIn n x = case force n x of
   100:                    Just b  -> b
   101:                    Nothing -> True
   102: 
   103: -- Our test
   104: closedTest n x = closed x ==> notFalseIn n (fmap closed (eval x)))
   105: ```
   106: 
   107: We run this and get the following result:
   108: 
   109: ```haskell
   110: depthCheck 5 closedTest
   111: LSC: Counterexample found after 49175 tests.
   112: Var 0: S (S _)
   113: Var 1: Lam (Lam (Var 1)) :@ Lam (Lam (Lam (Const _)))
   114: *** Exception: ExitFailure 1
   115: ```
   116: 
   117: This tells us that it found a counterexample when `n` is at least 2 and `x` is `Lam (Lam (Var 1)) :@ Lam (Lam (Lam (Const _)))` (for any wildcard '_'). These incomplete results are thanks to [Lazy SmallCheck] [8], which tries to identify exactly which parts of the input cause the failure.
   118: 
   119: [7]: https://github.com/UoYCS-plasma/LazySmallCheck2012
   120: 
   121: Why does this fail? Well, when this Term is evaluated, we get `Later (Now (Lam (Var 1)))`. There are two Partial wrappers around the Term, which is why the error only appears after forcing at least 2 steps. The Term itself, `Lam (Var 1)`, is not closed since a single Lam only gives us access to `Var 0`. This should never happen, so what's gone wrong?
   122: 
   123: The input contains an 'outer' function `Lam (Lam (Var 1))` which is returning an 'inner' function, which in turn returns the 'outer' function's argument. The outer function is applied to another Term, so we get back the inner function. However, this inner function needs some way to reference the outer function's argument, but our Term datatype doesn't give us anywhere to store this reference. Instead, we're forced to throw it away, so the `Var 1` Term is left 'dangling' as a free variable, which violates our property.
   124: 
   125: I tried keeping track of the environment inside the constructors of Term, but everything got very messy very quickly, so I gave up and Googled around for a solution. I found [this presentation] [8] which contains an interpreter for a quite complex language, featuring native booleans and integers, primitive operations on those types, `while` and `until` loops, explicit recursion, etc. Most of this is unnecessary for what I want, but it does feature lambdas, application, variables and an evaluator which uses `Partial` (although the presentation calls it `Delay`). Here's a stripped-down version, with the extraneous stuff removed:
   126: 
   127: [8]: http://www.cs.ox.ac.uk/ralf.hinze/WG2.8/22/slides/tarmo.pdf
   128: 
   129: ```haskell
   130: type Name = String
   131: 
   132: data Term = Var Name
   133:           | Lam Name Term
   134:           | Term :@ Term
   135: 
   136: data Val = F (Val -> Partial Val)
   137: 
   138: type Env = [(Name, Val)]
   139: 
   140: eval' :: Term -> Env -> Partial Val
   141: eval' (Var   x)   env = return (unsafelookup x env)
   142: eval' (Lam x f) env = return (F (\\a -> eval' f (update x a env)))
   143: eval' (f :@  x)  env = do F f' <- eval' f env
   144:                          x'   <- eval' x env
   145:                          f' x'
   146: ```
   147: 
   148: One problem that's immediately apparent is that `Term` has lost the `Const` constructor. Since Const values are opaque to the interpreter, they're simple enough to add back in:
   149: 
   150: ```haskell
   151: type Name = String
   152: 
   153: data Term a = Var Name
   154:             | Lam Name (Term a)
   155:             | Term a :@ Term a
   156:             | Const a
   157: 
   158: data Val a = F (Val a -> Partial (Val a))
   159:            | C a
   160: 
   161: type Env a = [(Name, Val a)]
   162: 
   163: eval' :: Term a -> Env a -> Partial (Val a)
   164: eval' (Const x) env = return (C x)
   165: eval' (Var   x) env = return (unsafelookup x env)
   166: eval' (Lam x f) env = return (F (\\a -> eval' f (update x a env)))
   167: eval' (f :@  x) env = do F f' <- eval' f env
   168:                          x'   <- eval' x env
   169:                          f' x'
   170: ```
   171: 
   172: Now let's switch to de Bruijn indices instead of String variables:
   173: 
   174: ```haskell
   175: data Term a = Var Nat
   176:           | Lam (Term a)
   177:           | Term a :@ Term a
   178:           | Const a
   179: 
   180: data Val a = F (Val a -> Partial (Val a))
   181:            | C a
   182: 
   183: type Env a = [Val a]
   184: 
   185: eval' :: Term a -> Env a -> Partial (Val a)
   186: eval' (Const x) env = return (C x)
   187: eval' (Var   n) env = let Just x = lookUp n env in return x
   188: eval' (Lam   f) env = return (F (\\a -> eval' f (a:env)))
   189: eval' (f :@  x) env = do F f' <- eval' f env
   190:                          x'   <- eval' x env
   191:                          f' x'
   192: ```
   193: 
   194: Now this is looking very similar to my previous, broken solution. What's the difference? Rather than evaluating Terms into Terms, we evaluate them into "Vals", which can contain Haskell functions. These Haskell functions can be closures, with access to whichever `env` variable was in scope when they were defined; hence we're implementing closures in LC by using Haskell's own closures!
   195: 
   196: Unfortunately there is actually a bug in this interpreter, which prevents it satisfying the properties I want. Specifically, it never uses `Later` to delay a result! This means that, even though it builds a Partial result, it will still diverge because it tries to put everything in a `Now` constructor. This makes it dangerous to evaluate arbitrary Terms, since the evaluator may get caught in a loop. What we need to do is add a `Later` every time the evaluator performs beta-reduction, ie. in the `f :@ x` case:
   197: 
   198: ```haskell
   199: eval' :: Term a -> Env a -> Partial (Val a)
   200: eval' (Const x) env = return (C x)
   201: eval' (Var   n) env = let Just x = lookUp n env in return x
   202: eval' (Lam   f) env = return (F (\\a -> eval' f (a:env)))
   203: eval' (f :@  x) env = do F f' <- eval' f env
   204:                          x'   <- eval' x env
   205:                          Later (f' x')
   206: ```
   207: 
   208: With this addition, the interpreter is now safe to run on arbitrary Terms, which allows us to use property-checking tools like SmallCheck and [QuickCheck] [9].
   209: 
   210: [9]: http://en.wikipedia.org/wiki/QuickCheck
   211: 
   212: In fact we can make another change, to turn this from a [call-by-value] [10] interpreter into a [call-by-need] [11] interpreter. When we evaluate an application `f :@ x`, we are currently evaluating `f` to get a closure `F f'` then we are evaluating `x` to get a Val `x'`, then we are using application `f' x'` to get our result. This forces us to evaluate our argument `x`, even if it's never used. If `x` causes an infinite loop, then our program is guaranteed to get stuck in a loop.
   213: 
   214: [10]: http://en.wikipedia.org/wiki/Call_by_value#Call_by_value
   215: [11]: http://en.wikipedia.org/wiki/Call_by_value#Call_by_need
   216: 
   217: Instead, we can use call-by-need, like Haskell, such that we only evaluate `x` if it's needed. To do this we need to get rid of the line `x' <- eval' x env`, since that will wait for the evaluation of `x` to finish. We still need to evaluate `x`, and we still need to do it in the context of the correct `env`, so what can we do? The solution is to put *Partial* Vals in the environment, rather than fully-evaluated ones. Such an interpreter looks like this:
   218: 
   219: ```haskell
   220: -- Environments now contain Partial Vals
   221: type Env a = [Partial (Val a)]
   222: 
   223: -- Function Vals now accept Partial arguments
   224: data Val a = F (Partial (Val a) -> Partial (Val a))
   225:            | C a
   226: 
   227: -- Evaluation now takes an Environment of Partial Vals
   228: eval' :: Term a -> Env a -> Partial (Val a)
   229: eval' (Const c) env = Now (C c)
   230: eval' (Var   n) env = let Just x = lookUp env n in x
   231: eval' (Lam   f) env = Now (F (\\a -> eval' f (a:env)))
   232: eval' (f :@  x) env = do F f' <- eval' f env
   233:                       Later (f' (eval' x env))
   234: ```
   235: 
   236: Now we will only get trapped in infinite loops when they're unavoidable, and even then it won't cause Haskell to get stuck, since we're using the Partial monad.
   237: 
   238: Now we can use SmallCheck to test some properties of our interpreter. The ones I've tried so far, without issue, are:
   239: 
   240: ```haskell
   241: -- Helpers
   242: 
   243: -- Force n steps of evaluation
   244: evalN n x = force n (eval x)
   245: 
   246: -- Apply a function Val to a constant
   247: ($$) (F f) x = f (Now (C x))
   248: 
   249: -- Conservative and lax predicates for checking that y evaluates to x
   250: equalIn   n x y = trueIn     n (fmap (== C x) (eval y))
   251: notDiffIn n x y = notFalseIn n (fmap (== C x) (eval y))
   252: 
   253: -- Handy Terms
   254: 
   255: omega = Lam (Var 0 :@ Var 0) :@ Lam (Var 0 :@ Var 0)
   256: 
   257: yComb = Lam (Lam (Var 1 :@ (Var 0 :@ Var 0)) :@
   258:              Lam (Var 1 :@ (Var 0 :@ Var 0)))
   259: 
   260: -- Tests
   261: 
   262: evalCoterminates x n = let x' = evalN n x in
   263:                        closed x ==> isJust x' || isNothing x'
   264: 
   265: omegaDiverges n = evalN n (Lam ((0 :@ 0) :@ (0 :@ 0))) == Nothing
   266: 
   267: evalApp     x = trueIn  1 (fmap (== C x) (eval (Lam 0) >>= ($$ x)))
   268: 
   269: evalSteps   x = equalIn 3 x (Lam 0 :@ Lam 0 :@ Const x)
   270: 
   271: idWorks     x = equalIn 2 x (i :@ Const x)
   272: 
   273: yTerminates x = equalIn 7 x (yComb :@ Lam (Lam 0) :@ Const x)
   274: 
   275: trueWorks  n x y = let t = Lam (Lam (Var 1)) :@ Const x :@ y in
   276:                    closed t ==> notDiffIn n x t
   277: 
   278: falseWorks n x y = let f = Lam (Lam (Var 0)) :@ x :@ Const y in
   279:                    closed f ==> notDiffIn n y f
   280: 
   281: zeroWorks  n x y = let z = Lam (Lam (Var 1)) :@ Const x :@ y in
   282:                    closed z ==> notDiffIn n x z
   283: zFalse x = equalIn 5 x (zComb :@ Lam (Lam (Var 0)) :@ Const x)
   284: ```
   285: 
   286: I'm reasonably confident that this interpreter is correct, but if anyone can spot a problem (or an improvement) then please let me know!
   287: 
   288: As usual, the code for this is living in [Git] [12].
   289: 
   290: [12]: /git/lazy-lambda-calculus

Generated by git2html.