Yesterday evening I searched some articles on Y-combinator. That is a so interesting and powerful combinator.
I think that what is the essence of Y-combinator is that it can make a call itself. It has a parameter which is
just used to represent itself. Are you puzzled? OK. Let's look at its a implementation.
(define Y
(lambda (x)
((lambda (maker)
(x (lambda (arg) ((maker maker) arg))))
(lambda (maker)
(x (lambda (arg) ((maker maker) arg)))))))
Here!
(lambda (maker)
...
((maker maker) ...))
From above simplified code, what we can discover? The code will trap into a infinite loop if running. Yes,
very good. Why there are two (lambda (maker) ...) in the body of "Y". In actual fact, the two lambda express
is same to ((maker maker) ...), except that they are anonymous. Their main task is to launch the infinite loop.
The first (lambda (maker) ...) is the former of the second; the second is the latter of the first. Therefore,
the second will produce its latter by itself. So, that is a infinite sequence.
The (lambda (arg) ...) is also important. Let's look at another example.
(define F*
(lambda (func-arg)
(lambda (n)
(if (= n 0) 1
(* n (func-arg (- n 1)))))))
((Y F*) 5) => 120
The "F*" get a function as its argument. So, (lambda (arg) ...) pass into the "F*". In sense, the "F*" made
a fixpoint as its return value. The "func-arg" represent a recursion (or loop) engine.
So, why we say that the Y-combinator is powerful? It make us separate the code of a fixpoint and the code of
recursion when coding; it also combine they when running.
Let's step into the ((Y F*) 5). Firstly, let's discover what result (Y F*) make. The "F*" is a lambda express.
So, (Y F*) is like the following code.
((lambda (maker)
(F* (lambda (arg) ((maker maker) arg))))
(lambda (maker)
(F* (lambda (arg) ((maker maker) arg)))))
Let's use "A" represent (lambda (maker) (F* (lambda (arg) ((maker maker) arg)))) for help our analysing. All
right, we can get that code.
((lambda (maker)
(F* (lambda (arg) ((A A) arg))))
A))
The simplified code, we found (A A) is embeded in a lambda express, (lambda (arg) ((A A) arg)). At that point,
the infinite recursion is not produced at once. Yes, it is delayed due to that lambda experss. You can call up
how to implement a "delay" form.:-) OK. Let's go on.
Due to that lambda embracing (A A) the "F*" can get a lambda express as its argument. It is time to get the "F*"
inside. The "F*" also return a lambda express, which must be launched by giving a number. So, the "F*" return a
lambda express like that.
(lambda (n)
(if (= n 0) 1
(* n ((lambda (arg) ((A A) arg) (- n 1)))))
Let's go back the ((Y F*) 5). It is same to the following code.
((lambda (n)
(if (= n 0) 1
(* n ((lambda (arg) ((A A) arg) (- n 1))))) 5)
Evaluating this express will result in a recursion because the "n" is 5. So, it cause that.
(* 5 ((lambda (arg) ((A A) arg) (- 5 1)))))
((A A) 4)
The (A A) result in (F* (lambda (arg) ((A A) arg))) again. So, we can know the followning code.
(* 4 ((lambda (arg) ((A A) arg) (- 4 1)))))
Again ang again. Before launching the next recursion, the fixpoint function test whether the "n"
is 0, if true stop recursion and return 1; otherwise, it have to launch a recursive call again.
It result in a recursive calling chain, which just find its fixpoint. Returning from the fixpoint,
it actually work out the result of factoral.
OK. Now we can write a function in Y-combinator to find a maximum from a list of number.
(define M*
(lambda (f)
(lambda (ls)
(if (null? (cdr ls)) (car ls)
(max (car ls) (f (cdr ls)))))))