1

I'm working on a tiny lambda calculus engine which I want it to be lazy as Haskell. I'm trying to, at least for now, stick to Haskell's rules so that I don't have to rethink everything, but I don't want to do this blindly.

I understand Haskell will not evaluate a term until its value is needed. Here is my first doubt. I understand a value is "needed" when is an argument to a built in function (so in (func x), x is needed if func is a built in function and (func x) is needed) or because is a function to be called (so in (x y), x would be needed if (x y) is needed).

My second problem is, say I have this recursive function:

let st = \x -> (st x)

The way I've implemented it so far is, if I call this like (st "hi"), "hi" won't be evaluated but wrapped in a thunk which contains the term and its scope, that will be added as "x" to the scope of the body of st. Then, when evaluating st again, another thunk will be created around the x in (st x), which will contain the term x and its scope, which contains another definition of x, that is, "hi". This way, nested thunks will keep building up until I run out of memory.

I tested my code above in GHCI and memory was OK. Then I tested this:

let st = \x -> (st (id x))

and memory built up until the application crashed. So apparently GHCI (or Haskell?) only use thunks when the argument is a function call; in every other case it uses the term's value. Which is something I could easily implement.

Another option I thought of, is not allowing nested thunks by, right before evaluating a function call and creating thunks for the arguments, evaluating the whole current scope to make sure no new thunk will contain another thunk. I think this should still let me create infinite lists and get some (or all?) of the benefits of lazy evaluation, and would even prevent the application from crashing when the let st = \x.(st (id x)) function is called.

I'm sure there are plenty of ways to implement lazy evaluation, but is hard to figure out what the pros and cons of each way is. Is there some list containing the most common implementations of lazy evaluation together with their pros and cons? And also, how does Haskell do it?

Juan
  • 15,274
  • 23
  • 105
  • 187
  • 2
    Read up on Weak Head Normal Form and the like. E.g. [Simon Marlow's book discusses the topic nicely](http://chimera.labs.oreilly.com/books/1230000000929/ch02.html#sec_par-eval-whnf). – leftaroundabout Aug 13 '14 at 09:38

1 Answers1

1

This is far from a full answer, but maybe it'll be enough to make progress.

First, the function

let st = \x -> (st x)

binds st to a lambda. This may be a thunk pointing to a lambda, or it may not be (the Haskell Report only specifies non-strict evaluation. If the compiler can prove that evaluating a thunk early doesn't change the semantics of the program, it's free to do so; it's trivial to prove that a lambda in the source code can be evaluated to WHNF without changing the semantics).

Regardless, suppose you force evaluation of st "hi". After applying the lambda (beta reduction), the next step is st "hi". So this beta reduction loops endlessly, but it never creates new data. That is, there's no need to wrap anything in a thunk. So though it loops forever, this application doesn't allocate memory.

Compare this to

let st = \x -> (st (id x))

Here, if we beta reduce:

st "hi"
st (id "hi")
st (id (id "hi"))
st (id (id (id "hi")))

etc. Here, because the argument to st is never evaluated, it builds up an endless chain of thunks wrapping a new id application, consuming increasing memory.

I think the problem you're having with your implementation is that you're wrapping "hi" underneath the lambda. Instead, whatever produces "hi" should create the thunk which then gets passed around until it's evaluated. Then "hi" only gets wrapped once instead of at each step.

Edit: forgot to answer your first question, but I can't do better than @leftaroundabout's suggestion to read up on Weak Head Normal Form. There are other questions on SO too, e.g. Haskell: What is Weak Head Normal Form? and Weak head normal form and order of evaluation

Community
  • 1
  • 1
John L
  • 27,937
  • 4
  • 73
  • 88
  • The way I'm doing it is not even checking the arguments, just blindly wrapping them in a thunk that also contains the scope, so yeah, is wrapping "hi" and then the `x`, which contains "hi", and then the other `x`, which contains the previous `x` and so on. But what you're saying is I should check whether the term is a variable or not, and if so use the thunk it represents instead of wrapping it into another thunk, right? Is this what GHCi does (at least basically)? – Juan Aug 24 '14 at 08:11
  • @JuanLuisSoldi in ghc, thunks are a property of values; (almost) any value can either be evaluated or unevaluated (a thunk). In the second example, ghc would apply `id x` (it's this act of function application that creates the new value/thunk) and pass that value into the recursive call. It sounds like your compiler does something else, and I don't think I understand it well enough to make any suggestions. But your idea of deciding whether to create a new thunk or not based upon the term structure could work. – John L Aug 24 '14 at 17:30
  • Will it apply `id x`? Isn't it supposed to put it in a thunk without applying it because is lazy? And isn't this why GHCi crashes when I run it? – Juan Aug 24 '14 at 22:16
  • @JuanLuisSoldi sorry I'm not being clear. When I said that the function is applied, I mean that ghc does something like `let x' = id x in st x'`, with `x'` being the thunk passed into the next stage. (Function application is not the same as reducing the resultant value to WHNF). I'm not sure how ghc decides what doesn't need to be wrapped, this part happens in the STG phase of the pipeline which I'm not as familiar with. The full details are probably in http://research.microsoft.com/apps/pubs/default.aspx?id=67083 – John L Aug 24 '14 at 23:46