This article is publised in my wechat offical account first.
What is Y Combinator?
Y Combinator has an interesting behavior that is when Y applies to a function f, it will derive to f applies to the application of Y to f. That is as follows.
// Y combinator application
Y(f) ==> f(Y(f)) ==> f(f(Y(f))) ==> ...
What is the use of this behavior?
Think about that there is an reality situation that cannot be exactly represented in computer, for example the number Pi, there is only an approximation approaching to it. Then that function f can be seen as an refinement of the approximation approach, the evaluation of Y(f) is the exact result if we have enough resource, e.g. computation time and space.
Before we code that approximation of number Pi, we do something easier beforehand.
Implement Y Combinator in Swift Version one
Let us implement Y Combinator in Swift naively first.
func Y<A>
(_ f: @escaping (@escaping A) -> A )
-> A
{return f(Y(f)) }
See this code, the generic function Y, our Y combinator, has the body as f(Y(f)), the same behavior showed above.
Y(f) ==> f(Y(f))
Type Equations and Solution
But the type of function Y and f is quite complicated. Let's figure out why they look like that.
A function must have type form looks like B -> A, or A -> B if you prefer.
And because the behavior of Y, the type of Y(f) is the same as f(Y(f)), because they are the same value, just like the types of 1 + 1 and 2 are the same as Int.
The type equations are as follows:
// the type form of function
f : X -> Y
// because Y(f), that is Y has f: X -> Y as parameter, then
Y : (X -> Y) -> Z
// because f: X -> Y and Y: (X -> Y) -> Z, then
Y(f) : Z
// because Y(f) = f(Y(f)), then
f(Y(f)) : Z
Based on above equations, we derive the follows.
// because f(Y(f)): Z and Y(f): Z, then
f : Z -> Z
// becuase Y(f): Z and f: Z -> Z, then
Y : (Z -> Z) -> Z
// becuase Y: (Z -> Z) -> Z, and f: Z -> Z, then
Y(f) : Z
// becuase Y(f): Z, and f: Z -> Z, then
f(Y(f)) : Z
This time the type for f, Y, Y(f), f(Y(f)) are convergent with a type variable Z.
So we may implement our Y Combinator as follows:
func Y1<A>(_ f: @escaping (A) -> A )-> A { return f(Y1(f)) }
This is our naive implementation of our Y combinator.
Infinite Recursion and Breaker
But there is a problem of our Naive implementation, as complaint by our compiler.
"Function call causes an infinite recursion"
To break that infinite recursion, we need to know why there is infinite recursion. The key is f(Y1(f)) in the body of Y1. if the f(Y1(f)) executes, Y1(f) will then executes, which cause another f(Y1(f)) executes, then cause Y1(f) executes, goes on and on.
This time lazy evaluation comes rescure, that is to embed Y1(f) into a function then pass to f. When f needs the result of Y1(f), it calls that embedding function.
func Y2<A>(_ f: @escaping (@escaping @autoclosure () -> A) -> A )-> A {
return f(Y2(f))
}
This time, the compiler doesn't complaint Y2.
Factorial with Y Combinator
Now let use our derived Y Combinator to implement a Factorial function.
The behavior of Factorial function is follows:
factorial 1 = 1
factorial n = n * (factorial n - 1)
To implement this behavior with Y Combinator, we got this:
let factorial = Y2({ g in
return { (acc: Int) in
return { (x: Int) in
x == 0 ? acc : g() (acc * x) (x - 1)}}})(1)(5)
This code looks mysterious, right? what is g, acc, x ....? But when you execute that code, it works, it output the factorial of 5, which is 120 = 1 * 2 * 3 * 4 * 5. Interesting.
So what happened inside the execution. To remind us, we show the Y combinator again here with our factorial implementation together.
func Y2<A>
(_ f: @escaping (@escaping @autoclosure () -> A) -> A )
-> A
{ return f(Y2(f)) }
let factorial = Y2({ g in
return { (acc: Int) in
return { (n: Int) in
n == 0 ? acc : g() (acc * n) (n - 1)}}})(1)(5)
The argument enclosure { g in ... } is passed as parameter f in Y2. So what is the type of this enclosure { g in ... }. That is the key for our implementation of factorial. Type equations and solution again.
// Assume the type of target enclosure is X
{ g in ... acc in ... x in ... }: X
// And f is (() -> A) -> A, then
f = { g in ... acc in ... x in ... }: (() -> A) -> A
// in the body of Y2, f(Y(f)),
// and the first argument to f is Y(f), then
g = Y(f): () -> A
// the rest arguments acc and x should be in A{ acc in ... x in ... }: A
// And acc and x is Int, therefore
A = (Int) -> (Int) -> Int
// To sum up, the type X is
X = (() -> (Int) -> (Int) -> Int) -> ( (Int) -> (Int) -> Int )
// Same as f and enclosure
f: (() -> (Int) -> (Int) -> Int) -> ( (Int) -> (Int) -> Int )
// How about our Y2: ((() -> A) -> A) -> A
// Substitue (Int) -> (Int) -> Int into A we got.
Y2: ((() -> (Int) -> (Int) -> Int) -> ( (Int) -> (Int) -> Int ))-> ((Int) -> (Int) -> Int)
// By remove some parentheses and embedding enclosure.
Y2: ((Int -> Int -> Int) -> (Int -> Int -> Int))-> (Int -> Int -> Int)
Now we clear how it works internally.
// The first parameter g in f is Y2(f)
g = Y2(f)
// The second parameter acc in f is (1), as accumulator.
acc = Initial with 1
// The third parameter n in f is (5), as counter.
n = Initial with 5
The g, acc and n will be passed into next Y(f) for our f(Y(f)).
Number Pi with Y Combinator.
Cool, right? Let us return to our approximation use of Y Combinator, to code an approximation function for our Number Pi.
// An approximation approach of Pi
Pi / 4 = 1 - 1/3 + 1/5 - 1/7 + ... - (-1)^n / (2*n -1)
// Step 1
(Pi / 4)_1 = 1 = - (-1)^1 / (2 * 1 - 1)
// Step 2
(Pi / 4)_2 = 1 - 1/3 = (Pi/4)_1 - (-1)^2 / (2 * 2 - 1)
// Step 3
(Pi / 4)_3 = 1 - 1/3 + 1/5 = (Pi/4)_2 - (-1)^3/(2*3-1)....
So our implementation is follows:
// acc as (Pi/4)_i
// n shows how many iteration we need to finish.
// i shows which step is executing.
// the following is 5 iterations for approaching Pi/4.
let PiY2 = Y2({ g in
return { (acc: Decimal) in
return { (n: Decimal) in
return { (i: Decimal) -> Decimal in
let numberI = NSDecimalNumber(decimal: i)
let intI = Int(truncating: numberI)
let temp: Decimal = pow(-1, intI) / (2 * i - 1)
let nextN: Decimal = n - 1
let nextI: Decimal = i + 1
return n == 0 ? acc : g() (acc - temp) (nextN) (nextI)
}}}})(0)(5)(1)
print("Pi is \(PiY2 * 4)")
Cool, Right. That is our implementation of Y Combinator in Swift.
Of course, you can implement other Combinators in Swift as well. E.g. K S I
func K<A, B>(_ x: A) -> (B) -> A { return {_ in x}}
func S<A, B, C>(_ x: @escaping (A) -> (B) -> C)
-> (@escaping (A) -> B)
-> (A)
-> C {
return { y in
return { z in
(x(z)) (y (z))}}}
func I<A, B>(_ x: A, _:B) -> A {
let k: (A) -> (@escaping (B) -> A) -> A = K
let k1: (A) -> (B) -> A = K
let s: (@escaping (A) -> (@escaping (B) -> A) -> A)
-> (@escaping (A) -> ((B)-> A))
-> (A)
-> A = S
return s(k)(k1)(x)}