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.