Sei sulla pagina 1di 14

21/11/2018 Deriving the Y Combinator in JavaScript | Enlight

Deriving the Y Combinator

Login to Download Project & Start Coding

By Nathan Leung

JULY 05, 2018 // 1 0 3 9 5 V IE WS       

What is the Y Combinator?


In order to understand the Y combinator, one must first understand recursion. Here’s a
definition.

recursion (noun)
see “recursion”

Kidding! But nonetheless, this definition captures the essence of recursion: self-reference. In
our case — programming — recursion allows for the succint description of a repeating
process.

Recursion can be puzzling at first, but oftentimes, it allows us to write code that is closer to
the true intent of what we wish to do than an imperative, loopy style. Here are a few examples.

https://enlight.nyc/projects/y-combinator/ 1/14
21/11/2018 Deriving the Y Combinator in JavaScript | Enlight

/**
* Imperative style
*/
function factorialA(x) {
let result = 1;
// What exactly is happening here?
for (let i = x; i--; i >= 1) {
result *= x;
}
return x;
}

/**
* Recursive style
*/
function factorialB(x) {
if (x === 0) {
return 1;
}
// Recursive call (function calls itself!)
// Stays true to definition of factorial: x times the factorial of x - 1
return x * factorial(x - 1);
}

/**
* Imperative style
*/
function fibonacciA(n) {
// Term 0: 1, Term 1: 1, Term 2: 2
n = n - 1;
let prevTerm = 1;
let currTerm = 1;
// What does this loop do, exactly?
for (let i = 0; i < n; i += 1) {
let temp = prevTerm;
prevTerm = currTerm;
currTerm += temp;
}
return currTerm;
}

/**
* Recursive style
*/
function fibonacciB(n) {
if (n === 0 || n === 1) {
return 1;
}
// Definition of next Fibonacci term is sum of two previous
// Easy to follow!
return fibonacci(n - 2) + fibonacci(n - 1);
}

Many high-level languages, JavaScript included, support recursion. But some languages don’t,
and in the field of theoretical computer science, introducing recursion into non-recursive

https://enlight.nyc/projects/y-combinator/ 2/14
21/11/2018 Deriving the Y Combinator in JavaScript | Enlight

contexts is a fascinating problem.

This is where the Y combinator comes in: it provides a way to recurse — to self-reference
— even if self-reference is unsupported in the base language!

Eliminating Recursion
The Y combinator bestows the power of self-referential recursion unto languages which don’t
support it. So to derive it in JavaScript, let’s pretend a little — let’s pretend that JavaScript
doesn’t support recursion.

I’ve put the recursive factorial function given in the initial examples in the block below. If
everything goes well, by the end of this writeup we’ll have a tool that will allow this factorial
function to self-reference again — but most importantly, without any self-reference in the
function itself!

function factorial(x) {
if (x === 0) {
return 1;
}
return x * factorial(x - 1);
}

The best way to begin removing the recursion from  factorial  looks like factoring out the call
to  factorial  from the function body and making it a parameter, so let’s do that.

Bringing Our Own

// Bring your own factorial function!


function byoFactorial(f, x) {
if (x === 0) {
return 1;
}
return x * f(x - 1);
}

With this new factorial function,  byoFactorial , we have to “bring our own”
working  factorial  function  f  to pass into the new, non-recursive function as a parameter, to
replace our old self-referential call.

Of course, the problem now is that we don’t have a working  factorial function anymore. But
that doesn’t mean we have to give up! We can start small — from zero.

Let’s begin by creating a function  f , that when passed to  byoFactorial , will make it work, but
only for  x = 0 (0! = 1) .

https://enlight.nyc/projects/y-combinator/ 3/14
21/11/2018 Deriving the Y Combinator in JavaScript | Enlight

0! is defined to be equal to 1 — but why? It comes from combinatorics, the study of


counting and choosing. In combinatorics, the definition of  n!  is the number of
orders and ways  n  items can be chosen (i.e. permutations).

For instance,  2! = 2  is the number of ways you can select and order two items —
the first item, then the second; or the second item, then the first. By that same
token,  0!  is the number of ways you can select and order zero items — and you
can do that in only  1  way.

How can we create this function  f  for which  byoFactorial  will work for  x = 0 (0! = 1) ? Well,
closer inspection of our  byoFactorial  function shows that it already has a base case defined —
when  x  is  0 , it returns  1 . Moreover, when  x = 0 , no self-reference occured anyway in the
original  factorial  function — thus,  f  is not called.

That means we can pass any arbitrary function as  f . When  x = 0 ,  byoFactorial  will still
return  1  regardless.

Here are some suggestions.

function identity(x) {
return x;
}
function one(x) {
return 1;
}
function random(x) {
return Math.random();
}
// I like to use `random` because you'll
// immediately know if your function stops
// working - you'll get a long and ugly
// decimal. With the `identity` or `one` functions,
// problems with output are more likely to slip
// through. For the rest of this writeup, I'll
// use `random`.

byoFactorial(random, 0);
// => 1 - it works!
byoFactorial(random, 1);
// => 0.07382473983422022

So far, we’ve been passing a function  f  to  byoFactorial , but we can just as easily create
replacement  factorial  functions from  byoFactorial by  bind ing  f . Using this method, we’ll
create new functions which we can call with just one parameter — our desired factorial,  x .

In our case,  bind  takes two parameters: a  this  value and then the actual first
parameter for  byoFactorial ,  f . Since the body of  byoFactorial  doesn’t
access  this  at all, we can just set it to  null .

https://enlight.nyc/projects/y-combinator/ 4/14
21/11/2018 Deriving the Y Combinator in JavaScript | Enlight

function byoFactorial(f, x) {
if (x === 0) {
return 1;
}
return x * f(x - 1);
}

// Pass null because there's nothing specific we need to set `this` to


const factorialUpTo0 = byoFactorial.bind(null, Math.random);
// factorialUpTo0 will work with x = 0, and can be called like a regular function:
factorialUpTo0(0);
// => 1

Here’s what we get:

factorialUpTo0(0) => 1
// I passed Math.random as the first parameter so you'll probably get different results
factorialUpTo0(1) => 0.07382473983422022
factorialUpTo0(2) => 0.7045168327647198
factorialUpTo0(3) => 1.4614935135773812
factorialUpTo0(4) => 0.3311529571140488

As expected, it works for  0! , but nothing else. But it’s progress! We’re  1/infinity  done our
complete  factorial  function!

1/infinity  is bigger than you think! Ever wondered what the sum
of  1/2 + 1/3 + 1/4 ... + 1/infinity + 1/(infinity + 1) was? Yep, it’s  infinity .

function byoFactorial(f, x) {
if (x === 0) {
return 1;
}
return x * f(x - 1);
}

Let’s make  byoFactorial  work for both  0!  and  1! . In other words, how can we create a
function  f  that we can bind to  byoFactorial  that’ll allow it to return correct outputs
for  x = 0  and  x = 1 ?

First, let’s walk through what happens when we pass  x = 1  into  byoFactorial . For
whatever  f  we come up with,  byoFactorial(f, 1)  will return  1 * f(0) . So, all we need is a
factorial function  f  which returns the correct value for the case when  x = 0 . Do we already
have a factorial function which works for  0! ?

We do, and it’s called  factorialUpTo0 ! Let’s bind it to  byoFactorial . This will create a factorial
function which will work for both  x = 0  and  1  — or rather, all integer values from 0 up to 1
(this process will be easier to reason about if we think about it this way). We’ll call this
function  factorialUpTo1 .

https://enlight.nyc/projects/y-combinator/ 5/14
21/11/2018 Deriving the Y Combinator in JavaScript | Enlight

const factorialUpTo1 = byoFactorial.bind(null, factorialUpTo0);

Before we keep going, let’s refactor our  byoFactorial  function. Calling  .bind  is clunky
(especially with that  null !) and we can clean up our syntax by returning a function instead (in
essence, creating a “factory”).

function factorialFactory(f) {
return function factorial(x) {
if (x === 0) {
return 1;
}
return x * f(x - 1);
};
}

Now, we can create  factorialUpTo0  and  factorialUpTo1  functions with much cleaner syntax.

const factorialUpTo0 = factorialFactory(Math.random);


const factorialUpTo1 = factorialFactory(factorialUpTo0);
// Instead of
// const factorialUpTo1 = byoFactorial.bind(null, factorialUpTo0);

And we can make  factorialUpTo2 , too!

const factorialUpTo2 = factorialFactory(factorialUpTo1);

factorialUpTo2 will work with for  x = 2 , because it will return  2 * factorialUpTo1(1) , and we


already know that  factorialUpTo1  will work up to 1, so we also know that  factorialUpTo2  will
return correct outputs for all values up to 1 as well!

const factorialUpTo3 = factorialFactory(factorialUpTo2);

By the same token,  factorialUpTo3  will work for  3! , because it will


return  3 * factorialUpTo2(2) , and we already know that  factorialUpTo2 will work on all values
up to 2!

We can continue this pattern to create our complete, working, non-recursive factorial function.

const factorialUpTo3 = factorialFactory(factorialUpTo2);


const factorialUpTo4 = factorialFactory(factorialUpTo3);
const factorialUpTo5 = factorialFactory(factorialUpTo4);
const factorialUpTo6 = factorialFactory(factorialUpTo5);
const factorialUpTo7 = factorialFactory(factorialUpTo6);
const factorialUpTo8 = factorialFactory(factorialUpTo7);
const factorialUpTo9 = factorialFactory(factorialUpTo8);
const factorialUpTo10 = factorialFactory(factorialUpTo9);
// ...
https://enlight.nyc/projects/y-combinator/ 6/14
21/11/2018 Deriving the Y Combinator in JavaScript | Enlight

factorialUpTo10(5);
// => 120
factorialUpTo10(8);
// => 40320
factorialUpTo10(10);
// => 3628800
factorialUpTo10(11);
// => 28570479.681626465
factorialUpTo10(12);
// => 79140708.41594502

Note that the very first function  f  we passed to  factorialFactory  to
create  factorialUpTo0  (in my case,  Math.random ) is only called if we pass an  x  greater
than the upper bound of our  factorialUpTo{x}  function. If we keep passing our
newly created  factorialUpTo{x}  functions back into  factorialFactory , ad
infinitum,  Math.random  will never be called (because the upper bound will be at
infinity), and our function will work over all values of  x !

Here’s what  factorialUpTo10  looks like, expanded.

const factorialUpTo10 = factorialFactory(


factorialFactory(
factorialFactory(
factorialFactory(
factorialFactory(
factorialFactory(
factorialFactory(
factorialFactory(
factorialFactory(
factorialFactory(factorialFactory(Math.random))
)
)
)
)
)
)
)
)
);

So, it looks like we’ll need to apply  factorialFactory  to itself ad infinitum to get our
working  factorial  function. How can we do that though?

const factorialUpToInfinity = ???

Here Comes Y!
The Y combinator is here to save the day! The Y combinator is a function which applies
another function infinite times, and in simple terms can be defined like below.

https://enlight.nyc/projects/y-combinator/ 7/14
21/11/2018 Deriving the Y Combinator in JavaScript | Enlight

function y(f) {
return f(f(f(f(f(f(f(f(f(f(f(f(f(...)))))))))))));
}

Unfortunately, JavaScript can’t evaluate the above expression. We need to simplify it — and to
start, it helps to think about the definition of  y in the context of infinite sums.

A Math Problem

What’s the sum of  sqrt(2 + sqrt(2 + sqrt(2 + sqrt(2 + sqrt(2 ...))))) ?

The solution is simple. Let’s call the sum  S ,


so  S = sqrt(2 + sqrt(2 + sqrt(2 + sqrt(2 + sqrt(2 ...))))) .

Now, if we look closely at the expression, we can see  S  is a part of itself! The sum can
alternatively be written as  S = sqrt(2 + S) .

Squaring both sides, we get  S^2 = S + 2 . Moving all terms of  S  to the left side, we
get  S^2 - S - 2 = 0 . Factored, this is  (S - 2)(S + 1) = 0 , so the two solutions to this equation
are  S = -1  and  S = 2 . Since  sqrt returns the principal (non-negative) square root, our solution
must be  S = 2 .

We can apply this same principle to our original formulation of the Y-


combinator:  y(f) = f(f(f(f(f(f(f(f(f(f(f(f(f(...))))))))))))) . Looking closely, it’s clear
that  y(f)  is a part of the equation for  y(f) ! We can write the new equation for  y(f)  like
so:  y(f) = f(y(f)) .

We can wrap that expression in another function so we can explicitly pass our  x  parameter,
too.

function y(f) {
return function(x) {
return f(y(f))(x);
};
}

There’s another reason we can’t define our version of  y  as simply  y(f) = f(y(f)) .

function y(f) {
return f(y(f));
}

When we evaluate the above, we get this:

const tenFactorial = y(factorialFactory)(10);


// => RangeError: Maximum call stack size exceeded

https://enlight.nyc/projects/y-combinator/ 8/14
21/11/2018 Deriving the Y Combinator in JavaScript | Enlight

The problem is that JavaScript tries to evaluate  y(factorialFactory) , which never finishes,
before doing any computation. Let’s quickly walk through the evaluation.

function y(f) {
return f(y(f));
}

y(factorialFactory)(10);
factorialFactory(y(factorialFactory))(10);
factorialFactory(factorialFactory(y(factorialFactory)))(10);

This continues until we get the stack overflow error.

On the other hand, when we wrap the expression in an outer function to pass the
parameter  x , we also defer evaluation of  y(f) . The term for this type of outer function is a
“thunk”, which you may have heard from libraries like redux-thunk, which does the same thing
but for Redux, a popular state management library often paired with React.

function y(f) {
return function thunk(x) {
return f(y(f))(x);
};
}

Let’s walk through the thunked evaluation.

function factorialFactory(f) {
return function factorial(x) {
if (x === 0) {
return 1;
}
return x * f(x - 1);
};
}

y(factorialFactory)(10)

thunk(10) {
return factorialFactory(y(factorialFactory))(10)
}

factorialFactory(y(factorialFactory))(10)

factorialFactory(function thunk(x) {
return factorialFactory(y(factorialFactory))(x)
})(10)

// At this point, the first argument to factorialFactory


// has been fully evaluated (the thunk is the result of
// the evalation)! Now, the body of factorialFactory can
// actually be run.

https://enlight.nyc/projects/y-combinator/ 9/14
21/11/2018 Deriving the Y Combinator in JavaScript | Enlight

10 * (function thunk(x) {
return factorialFactory(y(factorialFactory))(x);
})(9)

10 * factorialFactory(y(factorialFactory))(9);

// And so on

The evaluation semantics of JavaScript are really the only thing preventing us from defining
the Y combinator as we had it originally ( y(f) = f(y(f)) ). In other languages, it’s completely
acceptable!

In Haskell, a functional language, the Y combinator can be defined as we did above


initially:  y(f) = f(y(f)) . The reason this is possible is because Haskell’s evaluation semantics
are different — Haskell is a lazy language. By default, it doesn’t fully evaluate expressions until
they are truly needed.

Here’s how Haskell would evaluate  y(factorialFactory)(10)  above, given that  y  was defined as
we had it originally.

// Our factorialFactory, in JavaScript for reference


function factorialFactory(f) {
return function factorial(x) {
if (x === 0) {
return 1;
}
return x * f(x - 1);
};
}

y(factorialFactory)(10)
# Haskell will initially unpack the call to y by one layer
factorialFactory(y(factorialFactory))(10)
# At this point, Haskell will look at the call to factorialFactory, instead of continuing to unpa
return 10 * y(factorialFactory)(9)
# Haskell will repeat the above process for the rest of the calls to y
# ...
return 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 * factorialFactory(y(factorialFactory))(0)
# At this point, factorialFactory encounters its base case and returns 1
# Haskell never computes the actual value of y(f)!

But There’s Still Recursion!


Our current definition  y  of the Y combinator still contains recursion, though! One of the main
selling points of the Y combinator is that it allows us to add support for recursion to
languages which don’t support it — so there must be a way to get rid of the recursive call.

https://enlight.nyc/projects/y-combinator/ 10/14
21/11/2018 Deriving the Y Combinator in JavaScript | Enlight

function y(f) {
return function thunk(x) {
return f(y(f))(x);
};
}

We could take the same approach as we did before, factoring out the recursive call into a
parameter and creating a “factory” function, but that won’t fly — as we saw in
the  factorial  example, we’d still need to apply that factory function ad infinitum to create a
functional  y .

Let’s take a closer look at  y , then. How can we define  y  in non-recursive terms? Well, we can
take advantage of the fact that while we are voluntarily shunning self-reference in function
bodies, we can still pass functions to themselves as parameters!

We’ll need to make a clever substitution, though. Instead of defining  y(f)  as we have it now,
let’s define it as  y(f) = x(x)(f) , for some other function  x . In simpler
terms,  y = x(x)  —  y  is  x  applied to itself.

// Here's our current, recursively-defined y


function y(f) {
return function thunk(n) {
return f(y(f))(n);
};
}

// Apply the substitution y = x(x)


// Call the parameter otherX since we don't want
// to confuse the outer x and inner x and to avoid
// variable shadowing
function x(otherX) {
return function y(f) {
return function thunk(n) {
return f(otherX(otherX)(f))(n);
};
};
}

Let’s try this out.

const newY = x(x);

// x(x)(factorialFactory)
const newFactorial = newY(factorialFactory);

newFactorial(10);
// => 3628800

It works! Let’s walk through the evaluation.

https://enlight.nyc/projects/y-combinator/ 11/14
21/11/2018 Deriving the Y Combinator in JavaScript | Enlight

function x(otherX) {
return function y(f) {
return function thunk(n) {
return f(otherX(otherX)(f))(n);
};
};
}

x(x)(f);

return function thunk(n) {


return f(x(x)(f))(n);
};

// Unthunked
f(x(x)(f));

// From first step above, we see that x(x)(f) = f(x(x)(f))


// We can apply this substitution
f(f(x(x)(f)));

// And again, and again... look familiar?


f(f(f(f(f(f(x(x)(f)))))));

It works! By applying  x  to itself, we avoid self-reference in the function body. Everything the
function needs to run comes from its parameters.

And with a little bit more refactoring and substitution, we’ve got the non-recursive Y
combinator!

// What we already have


function x(otherX) {
return function y(f) {
return function thunk(n) {
// By passing `x` to itself (in the form of `otherX`),
// we can technically "call" `x` inside the function
// body of `x` - but importantly, non-recursively! Of course,
// now that we've altered the parameters of `x` to take
// itself, we need to pass `otherX` to `otherX` in the
// return statement, too.
return f(otherX(otherX)(f))(n);
};
};
}

// Let's refactor
const y = x(x);

// Here's `x` agian


function x(otherX) {
return function y(f) {
return function thunk(n) {
return f(otherX(otherX)(f))(n);
};
};

https://enlight.nyc/projects/y-combinator/ 12/14
21/11/2018 Deriving the Y Combinator in JavaScript | Enlight

// Now, just substitute the body of `x`


// for each `x` in the statement `y = x(x)`
const y = (function(otherX) {
return function(f) {
return function thunk(n) {
return f(otherX(otherX)(f))(n);
};
};
})(function(otherX) {
return function(f) {
return function thunk(n) {
return f(otherX(otherX)(f))(n);
};
};
});

// If we want `y` to be usable on functions


// with any number of arguments, we can use
// the ES6 spread syntax over the arguments
const y = (function(otherX) {
return function(f) {
return function thunk(...args) {
return f(otherX(otherX)(f))(...args);
};
};
})(function(otherX) {
return function(f) {
return function thunk(...args) {
return f(otherX(otherX)(f))(...args);
};
};
});

// We can shorten it even more with ES6 arrow functions


const y = (x => f => (...a) => f(x(x)(f))(...a))(x => f => (...a) =>
f(x(x)(f))(...a)
);

And there you go! One of the most important concepts in theoretical computer science,
sitting on your lap in just a few characters.

const y = (x => f => (...a) => f(x(x)(f))(...a))(x => f => (...a) =>
f(x(x)(f))(...a)
);

Appendix
Try writing your own  y -compatible, non-recursive implementations of your favorite recursive
processes! Here’s an example with the Fibonacci sequence mentioned earlier.

https://enlight.nyc/projects/y-combinator/ 13/14
21/11/2018 Deriving the Y Combinator in JavaScript | Enlight

function fibonacciFactory(f) {
return function thunk(n) {
if (n === 0 || n === 1) {
return 1;
}
return f(n - 2) + f(n - 1);
};
}

const fibonacci = y(fibonacciFactory);


fibonacci(10);
// => 89

About the Author


Thanks for reading! My name is Nathan Leung. I’m the founder of location-based social
messaging app Suap and an incoming first-year student at the University of Michigan.

You can learn more about me on my LinkedIn or on my personal website, and I encourage you
to try Suap at getsuap.com!

https://enlight.nyc/projects/y-combinator/ 14/14

Potrebbero piacerti anche