GHC's Data.Function
package provides an interesting function fix
, defined
as:
fix f = f $ fix f
Wait, what? This thing's supposed to be useful?
The purpose of this function (a "fixed-point combinator") is to abstract away recursion, and therefore allow us evaluate a recursive function defined non-recursively. In other words, it allows us to express a recursive function (i.e., a function that refers to itself) without ever giving that function a name. Here's how: let's say we have our classic recursive function calculating factorials:
factorial x = case x of 0 -> 1
n -> n * factorial (n-1)
If we want to express this function anonymously (and therefore non-recursively), we'd want to write:
\x -> case x of 0 -> 1
n -> n * factorial (n-1)
But then, factorial
is undefined. So what we do is change factorial
to an
argument f
, leaving the task of figuring out what f
is to the caller:
anon_factorial :: (Int -> Int) -> Int -> Int
anon_factorial = \f x -> case x of 0 -> 1
n -> n * f (n-1)
Note from the type definition that the factorial function is extracted by
applying anon_factorial
to the factorial function. This is a perfect
application of recursion - fixed_factorial
is our new factorial function:
fixed_factorial :: Int -> Int
fixed_factorial = anon_factorial fixed_factorial
This expands trivially to the recursive definition of factorial
given above.
The elegance of the solution is that, although we still had to use recursion in
the end, we separated that "dirty" task out from the actual work of calculating
the factorial function. Of course, we can do a bit better, and generalize
fixed_factorial
to a function fix
which takes any recursive function
defined as anon_factorial
function was, and converts it to a
conventionally-callable function:
fix f = f $ fix f
This function is important not because it's particularly useful (it's not), but because by allowing recursive functions to be defined non-recursively, it encapsulates the very essence of recursion, and nothing else. In a sense, it is the simplest non-trivial recursive function.
Note on Laziness
The fixed-point combinator defined here couldn't actually be evaluated with a special property of Haskell called laziness. Python, for instance, would see
def fix(f, x):
return f(fix(f, x))
and reduce fix(anon_factorial, 5)
to anon_factorial(fix(anon_factorial,
5))
. At that point python thinks the next thing to evaluate is the inner
expression: fix(anon_factorial, 5)
. And so we hit infinite recusion.
Haskell, however, will not attempt to simplify until it absolutely needs to.
Thus, fix anon_factorial
will remain so, until it is applied to a value. So
(fix anon_factorial) 5
will expand to anon_factorial (fix anon_factorial)
5
, at which point haskell will expand the outer anon_factorial
, rather than
the inner one. That makes all the difference:
(fix anon_factorial) 5
anon_factorial (fix anon_factorial) 5
case 5 of 0 -> 1
n -> n * (fix anon_factorial) (5-1)
5 * (fix anon_factorial) 4
And so progress is actually made.