Inspired by this article, I finally figure out what a Y Combinator is, and decide to write this post which covers my understanding on Y Combinator (implicit recursive function call and explicit type recursion) and examples in Scheme and Scala.

What is a combinator

A combinator is a function with no free variables (or with all variables bounded), a pure lambda expression that refers only to its arguments.

What is a Y Combinator

Y Combinator is a combinator which takes a single argument which is a function that is not recursive and turns back a version of that argument which is recursive. So it is clear that Y Combinator is a higher order function.

And actually the input function is also a higher order function. What the Y Combinator does is to return the fix-point-of the input function. That is to say:

1
(Y f) = (f (Y f))

Y Combinator directly computes out the fix-point of f.

Example of factorial function in Scheme

First, we define a almost-factorial function as:

1
2
3
4
5
6
(define almost-factorial
    (lambda (f)
        (lambda (n)
            (if (= n 0)
                1
                (* n (f (- n 1))))))

As we can see, the type of almost-factorial is (Int => Int) => Int => Int. Our aim is to define a Y such that (Y almost-factorial) is the factorial function with type Int => Int and the factorial function is the fix-point-of almost-factorial which means:

1
2
3
4
factorial = (almost-factorial factorial)
          = (almost-factorial
                (almost-factorial
                    (almost-factorial ...)))

Before derive the Y Combinator, let’s define another function called part-factorial:

1
2
3
4
5
6
7
8
(define part-factorial
    (lambda (self)
        (lambda (n)
            (if (= n 0)
            1
            (* n ((self self) (- n 1)))))))

((part-factorial part-factorial) 5) ; => 120

We can see that with (part-factorial part-factorial), we get a definition of factorial function. The input argument of part-factorial is a function named self, and (self self) has the type Int => Int. So, let’s assume the type of self is X => Int => Int, from (self self) we know that X = X => Int => Int. And it is clear that the type of part-factorial is also X => Int => Int, so that (part-factorial part-factorial) has the type Int => Int.

Let’s utilize the definition of almost-factorial:

1
2
3
4
5
(define part-factorial
    (lambda (self)
        (almost-factorial (self self))))

((part-factorial part-factorial) 5) ; => 120

Then we can define factorial as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(define factorial
    ((lambda (self)
        (almost-factorial (self self)))
     (lambda (self)
        (almost-factorial (self self)))))

;; or you can do the follows
;; (define factorial
;;     ((lambda (self)
;;         (self self))
;;      (lambda (self)
;;         (almost-factorial (self self)))))

(factorial 5) ; => 120

Finally, we can define Y Combinator as follows:

1
2
3
4
5
6
7
8
(define Y
    (lambda (f)
        ((lambda (h)
            (h h))
         (lambda (g)
            (f (g g))))))

((Y almost-factorial) 5) ; => 120

Let’s figure out what happens, (lambda (h) (h h)) defines an anonymous function which applies its argument to the argument itself, it corresponds to the (part-factorial part-factorial) part for obtaining a definition of factorial. From here, we can image that if the final result is a function with type A => B, then the return type of h should be A => B. Let’s assume the type of h is X => A => B, then we get a type recursion X = X => A => B because of (h h).

And (lambda (g) (f (g g))) corresponds to the definition of part-factorial which enables the explicit self call reference to obtain the fix-point-of f, where f is of the type (A => B) => A => B. Apply (lambda (g) (f (g g))) to (lambda (h) (h h)), we finally get the fix-point-function with type A => B. We can also notice that the return type of (g g) should be A => B because of (f (g g)) and the fact that the type of the input argument of f is A => B meanwhile the return type of f is A => B as well. And we will apply this anonymous function to (lambda (h) (h h)), so the type of g is also X => A => B.

Well, this is a definition of Y Combinator. With the respect to the type within the definition, you will find an infinite type recursion, fortunately the Scheme is a dynamical type language, so that it will not be a big problem.

And the above definition works in the lazy version of Scheme, for strict evaluation Scheme define Y as:

1
2
3
4
5
6
(define Y
    (lambda (f)
        ((lambda (x) (x x))
         (lambda (x)
            (f (lambda (y)
                    ((x x) y)))))))

Example of factorial function in Scala

Let’s first deal with the easy part, define the almostFactorial:

1
val almostFactorial = (f: Int => Int) => (n: Int) => if(n == 0) 1 else n * f(n - 1)

Then, let’s try to define Y. Since Scala is a statically typed language, the recursion on type level will cause invalid cyclic reference. So it’s hard to define Y through higher kinded type. Fortunately, it is possible to define Y with the help of case class:

1
2
3
4
5
6
7
8
9
10
def Y[A, Z] = (f: (A => Z) => A => Z) => {
    case class H(h: H => A => Z){
        def apply(h: H) = this.h(h)
    }
    val h = H(x => f(x(x)(_)))
    h(h)
}

val factorial = Y(almostFactorial)
println(factorial(5)) // 120

I think it’s ok to use case class to break the self reference on type level since in Scala functions are taken as objects.