How does x ever come to mean fix x in the first definition?
fix f = let x = f x in x
Let bindings in Haskell are recursive
First of all, realize that Haskell allows recursive let bindings. What Haskell calls "let", some other languages call "letrec". This feels pretty normal for function definitions. For example:
ghci> let fac n = if n == 0 then 1 else n * fac (n - 1) in fac 5
120
But it can seem pretty weird for value definitions. Nevertheless, values can be recursively defined, due to Haskell's non-strictness.
ghci> take 5 (let ones = 1 : ones in ones)
[1,1,1,1,1]
See A gentle introduction to Haskell sections 3.3 and 3.4 for more elaboration on Haskell's laziness.
Thunks in GHC
In GHC, an as-yet-unevaluated expression is wrapped up in a "thunk": a promise to perform the computation. Thunks are only evaluated when they absolutely must be. Suppose we want to fix someFunction
. According to the definition of fix
, that's
let x = someFunction x in x
Now, what GHC sees is something like this.
let x = MAKE A THUNK in x
So it happily makes a thunk for you and moves right along until you demand to know what x
actually is.
Sample evaluation
That thunk's expression just happens to refer to itself. Let's take the ones
example and rewrite it to use fix
.
ghci> take 5 (let ones recur = 1 : recur in fix ones)
[1,1,1,1,1]
So what will that thunk look like?
We can inline ones
as the anonymous function \recur -> 1 : recur
for a clearer demonstration.
take 5 (fix (\recur -> 1 : recur))
-- expand definition of fix
take 5 (let x = (\recur -> 1 : recur) x in x)
Now then, what is x
? Well, even though we're not quite sure what x
is, we can still go through with the function application:
take 5 (let x = 1 : x in x)
Hey look, we're back at the definition we had before.
take 5 (let ones = 1 : ones in ones)
So if you believe you understand how that one works, then you have a good feel of how fix
works.
Is there any advantage to using the first definition over the second?
Yes. The problem is that the second version can cause a space leak, even with optimizations. See GHC trac ticket #5205, for a similar problem with the definition of forever
. This is why I mentioned thunks: because let x = f x in x
allocates only one thunk: the x
thunk.
fix1 (1:) !! 1000000
andfix2 (1:) !! 1000000
. – Venose