Javascript derivation of Y-Combinator (from Jim Weirich) and ycombinator
Anyone familiar with functional programming knows about lambda expressions. The lambda expressions in programming languages are derived from the lambda algorithm invented by Qiu Qi in 1936. Y-Combinator is the most mysterious function in lambda calculus. It is used to implement recursive function calls in lambda calculus with only anonymous functions. Many people have deduced Y-Combinator. For example, this Javascript derivation Deriving the Y Combinator in 7 Easy Steps (Chinese) in addition, this article uses Scheme to translate Chinese into Javascript to deduce The Why of Y (Chinese ). I have read and found that the deduction process is a little too fast, and each step does not give enough explanation. This article shows you how to reconstruct a common factorial function repeatedly to export the Y-Combinator. The article was derived from a sharing (Youku) by Jim Weirich at ruby conference 2012. Jim Weirich is an important contributor to the Ruby community, developed a very popular Rake-a development tool used by almost all Ruby developers. Unfortunately, Jim Weirich died on August 1, February 19 this year. This derivation is great because the refactoring method is fully introduced before the derivation, and the entire derivation process becomes very smooth. So I also learned to write this article. This article consists of three parts: 1. A brief introduction to lambda calculus and Y-Combinator, which can be skipped directly, 2 reconstruction method, and 3 derivation of Y-Combinator process. The last two parts are shared by Jim Weirich, but some modifications are made for ease of understanding.
1 about lambda Calculation
Lambda Algorithm
This article does not detail lambda calculus and Y-Combinator, which may take a considerable amount of space. Unfamiliar students can learn by themselves through online materials (refer to the English wiki Chinese wiki Programming ages and Lambda Calculi G9 blog ). According to the definition of wiki: lambda calculus (lambda calculus, lambda-calculus) is a set of formal systems used to study function definitions, function applications, and recursion. Qiu Qiyun used the lambda algorithm to give a negative answer to the determination question in 1936. This kind of calculation can be used to clearly define what a computable function is. Lambda calculus is a formal system that has existed for a long time before the computer was built. Lambda calculus and Turing Machine abstract computing capabilities in two different directions. The former originated from theory and the latter from hardware design. In comparison, lambda calculus is simpler and more elegant and close to mathematics. In a word, it can be well summarized: lambda calculus can be called the smallest universal programming language.
Relationship between lambda calculus and Y-Combinator
Y-Combinator is a function that generates a recursive call to its own function for anonymous functions. Y-Combinator enables lambda calculus to express recursive logic. Because lambda calculus has the same computing power as the Turing Machine, lambda calculus can certainly represent any common programming concept, for example, the Qiu odd number used to represent the natural number and the Qiu qiboolean value used as the predicate (refer to Qiu odd number chilch_encoding ), or use single-parameter lambda to represent the Currying of multi-parameter functions (refer to Chinese wiki or talk about Currying ).
Understand the significance of Y-Combinator
Do we know how Y-Combinator works? The answer to negative energy is: it is almost useless. In programming languages with naming capabilities, such recursive functions are not required at all, and the underlying implementation of programming languages is based on the Turing machine model, which has no relationship with lambda calculus and Y-Combinator. The answer to positive energy is: it is helpful to help you better understand functional programming.
Lambda and Javascript
Lambda expressions in modern programming languages are just named from lambda calculus, which is quite different from the original lambda calculus. Javascript does not have any syntax specifically to indicate that lambda only writes such a nested function "function {return function {...}}"
Interesting things
Lambda is painted on the cover of the film. The emblem of MIT's Computer Science Department is Y-Combinator, And the incubator of Paul Graham, the godfather of entrepreneurship, is Y-Combinator.
2. Reconstruction Method
The following five methods are all mentioned by Jim Weirich. The five methods are used at least once in subsequent derivation. The first method is most frequently used.
1 Tennent corresponsponprinciple
Wrap any expression in a lambda and immediately call this lambda expression without affecting the value of the original expression. This method is used many times during the derivation of Y-Combinator.
// Tennent Correspondence Principlemul3 = function(n) { return n * 3 }mul3 = function(n) { return function() { return n * 3 }() }make_adder = function(x) { return function(n) { return n + x }}make_adder = function(x) { return function() { return function(n) { return n + x } }()}
2 Introduce Binding
You can add a parameter to a lambda expression without parameters. Of course, this parameter is not bound to any lambda, and then you can pass in any value for this new parameter during the call, the modified code will not affect the original expression. This is easy to understand, because the new parameter is added later, and lambda is not used at all. Unused values certainly do not affect the expression.
// Introduce Bindingmake_adder = function(x) { return function() { return function(n) { return n + x } }()}make_adder = function(x) { return function(xyz) { return function(n) { return n + x } }(123456)}
3 Rebind
When several lambda with nested relationships exist, you can add a parameter to the lambda without parameters in the middle layer, and then pass the external parameters to the internal lambda through the newly added parameters, rebind indicates re-binding from the name. In the code below, n is originally bound to n of the outermost layer. After the Rebind is modified, it is bound to n of the intermediate layer, the n of the outer layer does not affect it. It obtains the value through the n passed in when the middle layer is called.
// Rebindmul3 = function(n) { return function() { return n * 3 }() }mul3 = function(n) { return function(n) { return n * 3 }(n) }
4 Function Wrap
This is a bit similar to the first method Tennent corresponsor Principle, but not quite the same. You can use a lambda to wrap the original lambda, as long as you call the packaged lambda.
// Function Wrapx = function(x) { return function(n) { return n + x }}x = function(x) { return function(z) { return function(n) { return n + x }(z) }}
5 Inline Function
Inline is the best way to understand. Replace the original variable with the variable content, that is, change the named variable to anonymous.
//5 Inline Functioncompose = function(f, g) { return function(n) { return f(g(n)) }}mul3add1 = compose(mul3, add1)compose = function(f, g) { return function(n) { return f(g(n)) }}mul3add1 = function(f, g) { return function(n) { return f(g(n)) }} (mul3, add1)
3. Derivation of Y-Combinator Process
Note: every time the results are output in this form, it is very convenient for me to run with nodejs, and there is no problem in direct browsers. At the same time, for ease of understanding, do not change the name of Fixed-purpose variables from the beginning to the end (variables are often renamed during Jim Weirich's derivation, which is easy to confuse people ).
console.log(function(){ //return xxx;}())
First, let's take a look at what the Javascript version of Y-Combinator looks like.
In the first version, this is a very common recursive factorial function, with nothing special.
console.log(function(){ function fact(n){ return n == 1 ? 1 : n * fact(n-1); } return fact(5)}())
We put the fact in the parameter, so that a free variable is missing in the function body. Changing the name variable from a free variable to a bound variable is an important reconstruction method.
// method ==> parameterconsole.log(function(){ function fact(g, n){ return n == 1 ? 1 : n * g(g, n-1); } return fact(fact, 5)}())
Furthermore, we change the fact return value from a number to a function, that is, the fact changes from "calculate a factorial function of a number" to "return a function that can calculate a factorial ", after this reconstruction, n and fact are split, so that the logic is clearer and the calculation factorial action is divided into two steps: the first step is to create a function that can calculate factorial, this is done by fact (fact); the second step is to let this function calculate the factorial of 5. The call method has undergone minor changes, from fact (fact, 5) to fact (fact) (5 ).
// parameter ==> lambdaconsole.log(function(){ function fact(g) { return function(n) { return n == 1 ? 1 : n * g(g)(n-1); } } return fact(fact)(5)}())
Take a break and perform a simple refactoring. The name of fact is called fx, and fx will be called later.
// naming fact(fact)console.log(function(){ function fact(g) { return function(n) { return n == 1 ? 1 : n * g(g)(n-1); } } fx = fact(fact) return fx(5)}())
For the first time, this step uses the Tennent corresponsor Principle (TCP for short), which is the reconstruction method of the preceding five methods. It wraps the return fact (fact) and calls it.
// Tennent Correspondence Principleconsole.log(function(){ function fact(g) { return function(n) { return n == 1 ? 1 : n * g(g)(n-1); } } fx = function() { return fact(fact) }() return fx(5)}())
In this step, replace the free variable fact with the bound variable g, which has been used before.
// free variable ==> parameterconsole.log(function(){ function fact(g) { return function(n) { return n == 1 ? 1 : n * g(g)(n-1); } } fx = function(g) { return g(g) }(fact) return fx(5)}())
In the Inline Reconstruction Method 5, the Inline result is that the name variable fact disappears and is replaced by a lambda expression where fact is needed.
// Inlineconsole.log(function(){ fx = function(g) { return g(g) } ( function(g) { return function(n) { return n == 0 ? 1 : n * g(g)(n-1) } } ) return fx(5)}())
Next, TCP wraps and executes the innermost lambda layer. The parameter is blank during execution.
// Tennent Correspondence Principleconsole.log(function(){ fx = function(g) { return g(g) } ( function(g) { return function(n) { return function() { return n == 0 ? 1 : n * g(g)(n-1) }() } } ) return fx(5)}())
For a Rebind, pass the outer n to the inner lambda.
// Rebindconsole.log(function(){ fx = function(g) { return g(g) } ( function(g) { return function(n) { return function(n) { return n == 0 ? 1 : n * g(g)(n-1) }(n) } } ) return fx(5)}())
Again, TCP. Note that the wrapped lambda is return function (n) {return n = 0? 1: n * g (g) (n-1)}, so the 13th rows are written as} () (n) instead of} (n )().
// Tennent Correspondence Principleconsole.log(function(){ fx = function(g) { return g(g) } ( function(g) { return function(n) { return function() { return function(n) { return n == 0 ? 1 : n * g(g)(n-1) } }()(n) } } ) return fx(5)}())
Use Introduce Binding to add a recursion_in_mind parameter for a function without parameters, and replace it with g (g). Of course, you need to pass g (g) in. Because the recursion_in_mind function has special meanings, we use a special name. This variable represents a hypothetical function that can recursively calculate factorial. Its purpose is described in the subsequent steps.
// Introduce Binding console.log(function(){ fx = function(g) { return g(g) } ( function(g) { return function(n) { return function(recursion_in_mind) { return function(n) { return n == 0 ? 1 : n * recursion_in_mind(n-1) } }(g(g))(n) } } ) return fx(5)}())
Again, TCP. Note that the package is lambda represented by the entire fx. Therefore, the brackets of the call are at the end of the 18 rows.
// Tennent Correspondence Principleconsole.log(function(){ fx = function() { return function(g) { return g(g) } ( function(g) { return function(n) { return function(recursion_in_mind) { return function(n) { return n==0 ? 1 : n * recursion_in_mind(n-1) } }(g(g))(n) } } ) }() return fx(5)}())
Once again, Introduce Binding passes in a parameter for the call added in the previous 18 rows. At the same time, this parameter does not use free variables, but uses a lambda directly, and the code is a little slow, you will see it carefully.
// Introduce Bindingconsole.log(function(){ fx = function(f) { return function(g) { return g(g) } ( function(g) { return function(n) { return f(g(g))(n) } } ) }( function(recursion_in_mind) { return function(n) { return n==0 ? 1 : n * recursion_in_mind(n-1) } } ) return fx(5)}())
The temp variable is used to represent the lambda directly imported in the previous step, and the fx is renamed to Y. Yes, this Y is a form of Y-Combinator. In addition, to facilitate reading of the Code, multiple lines of code are organized into one line. After this step is deduced, the code structure suddenly becomes different from the previous one. Now we can see that there are two naming functions: temp and Y. The calculation process is a clear two-step process. The first step is fact = Y (temp), which is made of temp and Y as the production process and generates a fact function, this step is in the manufacturing function. Step 2 Use 5 to call this newly created function. The temp function is also very special. It receives a recursive function recursion_in_mind, and then calls this function to calculate n-1 factorial. After this step is deduced, we can assume that Y-Combinator has been deduced. However, we can see that Y is obviously different from Y-Combinator in lambda calculus, therefore, further derivation is required.
// naming function(recursion_in_mind) {...}// Y is form one of Y-Combinatorconsole.log(function(){ temp = function(recursion_in_mind) { return function(n) { return n == 0 ? 1 : n * recursion_in_mind(n-1) } } Y = function(f) { return function(g) { return g(g) } ( function(g) { return function(n) { return f(g(g))(n) } } ) } fact = Y(temp) return fact(5)}())
This step is a little difficult to deduce, because the previous five methods of reconstruction are useless, and a new concept called "fixed point of function" needs to be introduced, the fixed point of function f is a value x, which makes f (x) = x. For example, 0 and 1 are the fixed points of the function f (x) = x ^ 2 (the calculated square function), because 0 ^ 2 = 0 and 1 ^ 2 = 1. Given that the fixed point of a first-order function (a function on a simple value such as an integer) is a first-order value, the fixed point of a higher-order function f is another function g that makes f (g) = g.
Back to the derivation, now we need a conclusion on the fixed point to complete the subsequent derivation, that is, temp (fact) = fact (you can ignore the need for such an intermediate conclusion first, the current task is to prove the correctness of this conclusion. When the conclusion is used, we can know why we need it.) What does this mean? According to the definition just mentioned, the fixed point of the Higher-Order Function temp is another function fact, so that temp (fact) = fact can be achieved as long as this step is equal. There is no problem with subsequent derivation.
Let's look back at the return fact (5) of the 19 rows. We can see that fact is a factorial function that can calculate 5 independently. What is temp? temp is a function that receives a function as a parameter and returns a function. Temp (fact) is to pass the fact into temp, that is, to pass in a function for calculating the factorial. by viewing the temp function, we can find that, temp uses this factorial function to calculate n-1 factorial. Then multiply n, and the return value is exactly a calculation of n! According to return fact (5), we have come to the conclusion that fact () is also a calculation of n! So we can draw this intermediate conclusion, that is, temp (fact) = fact. The intermediate conclusion is true!
According to the original plan, we should take the difficult temp (fact) = fact conclusion and start to deduce it. An interesting phenomenon will be inserted before going on, because of this intermediate conclusion, fact is the fixed point of the function temp, and the function fact is developed by the Y function to process the raw material temp, so we can define it from another angle as Y-Combinator, y-Combinator can calculate a non-moving (fact) function of a function (that is, the temp function ).
Continue on the road. Check the above Code for the 17 rows. We can see that the fact function is exactly the g (g) of the 12 rows, because return g (g) is assigned to the fact, the Y function is called in line 17 and passed in temp, that is, f in the Y function is temp. Come up with the intermediate conclusion that temp (fact) = fact is used, we can replace the return g (g) of the first row with return f (g )), because fact is the point of temp, g (g) is the point of f, so f (g) = g (g) is true. Therefore, this deduction only modifies one row, that is, the above 12 rows. Now, the reconstruction is complete.
// fixed point refactor g(g) ==> f(g(g))console.log(function(){ temp = function(recursion_in_mind) { return function(n) { return n == 0 ? 1 : n * recursion_in_mind(n-1) } } Y = function(f) { return function(g) { return f(g(g)) } ( function(g) { return function(n) { return f(g(g))(n) } } ) } fact = Y(temp) // temp(fact) = fact return fact(5)}())
In the last step, we need to use the return f (g) of the above 11 rows to perform the Function Wrap of the 5 reconstruction method. The input parameter is n. Through this modification, the Y function of the current Javascript version is exactly the same as Y in the standard lambda algorithm, which is very good. Again, let's look at what we gave before derivation.
// Function Wrap// Y is form two of Y-Combinatorconsole.log(function(){ temp = function(recursion_in_mind) { return function(n) { return n == 0 ? 1 : n * recursion_in_mind(n-1) } } // λg.(λx.g(x x))(λx.g(x x)) Y = function(f) { return function(g) { return function(n) { return f(g(g))(n) } } ( function(g) { return function(n) { return f(g(g))(n) } } ) } fact = Y(temp) return fact(5)}())
It is reasonable to say that the derivation process of Y has ended, but I think this is not perfect, because a named variable temp appears, Y is generated to implement recursive calls to anonymous functions. Here we show what temp means. Men and women are not recommended for searching. So we finally use Inline, directly throwing the lambda represented by temp to the Y function is a perfect demonstration of Y's true purpose: to create a recursion call to its own function (fact function) for anonymous functions ), the full text is complete.
// Inlineconsole.log(function(){ // λg.(λx.g(x x))(λx.g(x x)) Y = function(f) { return function(g) { return function(n) { return f(g(g))(n) } } ( function(g) { return function(n) { return f(g(g))(n) } } ) } fact = Y( function(recursion_in_mind) { return function(n) { return n == 0 ? 1 : n * recursion_in_mind(n-1) } } ) return fact(5)}())
What is Y-Combinator?
Y combination child