The slow fix calls f on each step of recursion, while the fast one calls f exactly once. It can be visualized with tracing:
import Debug.Trace fix f = f (fix f) fix' f = let x = f x in x facf :: (Int -> Int) -> Int -> Int facf f 0 = 1 facf f n = n * f (n - 1) tracedFacf x = trace "called" facf x fac = fix tracedFacf fac' = fix' tracedFacf
Now try some running:
> fac 3 called called called called 6 > fac' 3 called 6
In more detail, let x = f x in x results in a lazy thunk being allocated for x, and a pointer to this thunk is passed to f. On first evaluating fix' f, the thunk is evaluated and all references to it (here specifically: the one we pass to f) are redirected to the resulting value. It just happens that x is given a value that also contains a reference to x.
I admit this can be rather mind-bending. It's something that one should get used to when working with laziness.