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?