Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does GHC make fix so confounding?

Looking at the GHC source code I can see that the definition for fix is:

fix :: (a -> a) -> a
fix f = let x = f x in x

In an example fix is used like this:

fix (\f x -> let x' = x+1 in x:f x')

This basically yields a sequence of numbers that increase by one to infinity. For this to happen fix must be currying the function that it receives right back to that very function as it's first parameter. It isn't clear to me how the definition of fix listed above could be doing that.

This definition is how I came to understand how fix works:

fix :: (a -> a) -> a
fix f = f (fix f)

So now I have two questions:

  1. How does x ever come to mean fix x in the first definition?
  2. Is there any advantage to using the first definition over the second?
like image 825
Vanson Samuel Avatar asked Oct 21 '12 15:10

Vanson Samuel


2 Answers

It's easy to see how this definition works by applying equational reasoning.

fix :: (a -> a) -> a
fix f = let x = f x in x

What will x evaluate to when we try to evaluate fix f? It's defined as f x, so fix f = f x. But what is x here? It's f x, just as before. So you get fix f = f x = f (f x). Reasoning in this way you get an infinite chain of applications of f: fix f = f (f (f (f ...))).

Now, substituting (\f x -> let x' = x+1 in x:f x') for f you get

fix (\f x -> let x' = x+1 in x:f x')
    = (\f x -> let x' = x+1 in x:f x') (f ...)
    = (\x -> let x' = x+1 in x:((f ...) x'))
    = (\x -> x:((f ...) x + 1))
    = (\x -> x:((\x -> let x' = x+1 in x:(f ...) x') x + 1))
    = (\x -> x:((\x -> x:(f ...) x + 1) x + 1))
    = (\x -> x:(x + 1):((f ...) x + 1))
    = ...

Edit: Regarding your second question, @is7s pointed out in the comments that the first definition is preferable because it is more efficient.

To find out why, let's look at the Core for fix1 (:1) !! 10^8:

a_r1Ko :: Type.Integer    
a_r1Ko = __integer 1

main_x :: [Type.Integer]   
main_x =
  : @ Type.Integer a_r1Ko main_x

main3 :: Type.Integer
main3 =
  !!_sub @ Type.Integer main_x 100000000

As you can see, after the transformations fix1 (1:) essentially became main_x = 1 : main_x. Note how this definition refers to itself - this is what "tying the knot" means. This self-reference is represented as a simple pointer indirection at runtime:

fix1

Now let's look at fix2 (1:) !! 100000000:

main6 :: Type.Integer
main6 = __integer 1

main5
  :: [Type.Integer] -> [Type.Integer]
main5 = : @ Type.Integer main6

main4 :: [Type.Integer]
main4 = fix2 @ [Type.Integer] main5

main3 :: Type.Integer
main3 =
  !!_sub @ Type.Integer main4 100000000

Here the fix2 application is actually preserved:

fix2

The result is that the second program needs to do allocation for each element of the list (but since the list is immediately consumed, the program still effectively runs in constant space):

$ ./Test2 +RTS -s
   2,400,047,200 bytes allocated in the heap
         133,012 bytes copied during GC
          27,040 bytes maximum residency (1 sample(s))
          17,688 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)
 [...]

Compare that to the behaviour of the first program:

$ ./Test1 +RTS -s          
          47,168 bytes allocated in the heap
           1,756 bytes copied during GC
          42,632 bytes maximum residency (1 sample(s))
          18,808 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)
[...]
like image 153
Mikhail Glushenkov Avatar answered Sep 28 '22 03:09

Mikhail Glushenkov


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.

like image 29
Dan Burton Avatar answered Sep 28 '22 02:09

Dan Burton