Students familiar with functional programming understand lambda expressions, and lambda expressions in programming languages derive from the lambda calculus invented by Chow kit in 1936. Y-combinator is one of the most mysterious functions in lambda calculus. Its function is to implement recursive function calls in lambda calculus with only anonymous functions. Derivation Y-combinator A lot of people have done it, such as this JavaScript derivation deriving the Y Combinator in 7 easy Steps (Chinese) and the derivation of this text using scheme Chinese translation as JavaScript The Why of Y (Chinese). Read I found that the derivation process thinking jumps a bit large, and each step does not give sufficient explanation. This article shows the reader how to reconstruct a common factorial function and deduce the y-combinator. The derivation of the article comes from Jim Weirich's share of the Ruby Conference (Youku), Jim Weirich is an important contributor to the Ruby community, developing a very popular rake--almost all Ruby Developer-Used development tools. It is a pity that Jim Weirich died on February 19 this year. This derivation is great because the refactoring method is fully introduced before derivation, and the whole process becomes very flat. So I also by the way learn to write this article. The article is divided into three parts: 1 The simple introduction of lambda calculus and Y-combinator, the understanding of people can skip directly, 2 reconstruction method, 3 derivation y-combinator process. The latter two parts were mainly extracted from Jim Weirich, but some modifications were made for ease of understanding.
1 About lambda calculus
Lambda calculus
This article does not cover the lambda calculus and y-combinator in detail, which may take a considerable amount of space. Unfamiliar classmates can learn by themselves via online materials (see Wiki programming Languages and Lambda calculi G9 blog). As defined by the wiki: lambda calculus (lambda calculus,λ-calculus) is a formal system for studying function definitions, function applications, and recursion. Chow Kit uses lambda calculus to give a negative answer to the question of judgment over the past 1936 years. This calculation can be used to clearly define what a computable function is. Lambda calculus is a form of system that has existed for a long time before the computer was built. Lambda calculus and Turing are abstractions of computational power from two different directions, the former originating from theory and the latter from hardware design. In comparison, lambda calculus is simpler and more graceful and close to mathematics, a sentence that can be summed up well: lambda calculus can be called the smallest universal programming language.
The relationship between lambda calculus and Y-combinator
Y-combinator is a function that can generate a recursive call to its own function for an anonymous function. Y-combinator makes lambda calculus capable of expressing recursive logic. Because lambda calculus and Turing have the same computational power, lambda calculus can of course represent any common programming concept, such as the number of Chow kit used to represent natural numbers and the Chow kit Boolean values used as predicates (refer to Chow kit number church_encoding). Or currying with a single-parameter lambda representation of a multi-parameter function (refer to the Chinese wiki or talk about currying).
Understand the meaning of Y-combinator
Do we know y-combinator useful? The answer to the negative energy is: there is little use. There is no need for such a recursive function in a programming language with a naming capability, and the underlying implementation of the programming language is based on the Turing model, which has no relation to lambda calculus and y-combinator. The answer to positive energy is that a little bit of usefulness can help you better understand functional programming.
Lambda and JavaScript
Lambda expressions in modern programming languages are only named after the lambda calculus, which is quite different from the original lambda calculus. In JavaScript, there is no syntax specifically for Lambda to write only such nested functions as "function{return function{...}"
Interesting thing.
SICP cover Painting Lambda,mit Computer Science Department emblem is y-combinator, entrepreneurial godfather Paul Graham incubator called Y-combinator.
2 Methods of refactoring
The following 5 methods are mentioned by Jim Weirich, and 5 methods have been used at least once for subsequent derivation. The first method is most frequently used.
1 Tennent Correspondence Principle
Wrapping any expression in a lambda and immediately invoking the lambda expression does not affect the value of the original expression. This method was 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 an parameterless lambda expression, which is not bound to any lambda, and can then pass any value to the new parameter at the time of the call, so the modified code will not have an effect on the original expression. This is also easy to understand, because the new parameters are added later, the lambda is not used at all, there is no use of the value of course will not have any effect on 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 exist with a nested relationship, you can add an argument to a lambda with no parameters in the middle layer, and then pass the external parameter to the internal lambda,rebind by the newly added parameter, which is the meaning of the re-binding from the name, and the N in the code below is bound to the outermost n. Rebind is modified to bind to the middle tier N, the outer n has no effect on it, he is called through the middle tier when the N to get the value.
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 correspondence principle, but not quite the same, you can use a lambda to wrap the original lambda, just call the wrapped lambda.
function wrapx = function (x) { return function (n) {return n + x}}x = function (x) { return function (z) {retur n function (n) {return n + x} (z)}}
5 Inline Function
Inline is best understood by replacing the variable name with the contents of the variables, which means that the named variable becomes anonymous.
5 Inline functioncompose = function (f, g) { return function (n) {return F (g (n))}}mul3add1 = Compose (Mul3, ADD1) Comp OSE = function (f, g) { return function (n) {return F (g (n))}}mul3add1 = function (f, g) { return function (n) {Retu RN F (g (n))}} (Mul3, ADD1)
3 derivation of the y-combinator process
Explain, each time the result is with such output form, I use Nodejs run very convenient, direct browser also no problem. At the same time, for the sake of understanding, fixed-use variables from start to end as far as possible not to modify the name (Jim Weirich derivation often rename variables, easy to confuse people).
Console.log (function () { //return xxx;} ())
First see if the JavaScript version of the Y-combinator, it is probably what it looks, follow-up can be easily compared.
The first version, which is a very common recursive factorial function, is nothing special.
Console.log (function () { function fact (n) { return n = = 1? 1:n * FACT (n-1); } return fact (5)} ())
We put the fact into the parameter so that the function body is less a free variable, it is a very important reconstruction method to change the named variable from free variable to bound variable.
Method ==> Parameterconsole.log (function () { function fact (g, N) { return n = = 1? 1:n * g (g, n-1); } return Fact (fact, 5)} ())
Further, we change the return value of fact from a number to a function, that is, the fact from "the function that computes the factorial of a number", which becomes the "function that returns a function that computes the factorial," after this refactoring, N and fact are split open, so that the logic is clearer, The calculation of factorial is decomposed into two steps: The first step is to create a function that calculates the factorial, which is done by fact, and the second step is to have the function calculate the factorial of 5. The invocation has undergone a subtle change from fact (fact, 5) to 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, come to a simple refactoring, name the fact, call FX, and call FX later.
naming fact Console.log (function () { function fact (g) { return function (n) { return n = = 1? 1:n * g ( g) (n-1); } } FX = fact return FX (5)} ())
This step first uses the previous learning refactoring 5 method of the Tennent correspondence Principle, referred to as TCP, the return fact (fact) wrapped up a call.
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)} ())
This step replaces the free variable fact with the bound variable g, which was 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)} ())
The Inline,inline of the refactoring 5 method is that the named variable of fact disappears and is replaced with 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)} ())
One more TCP, wrap and execute the most inner lambda, and the parameters are empty when executed.
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)} ())
To 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)} ())
One more time. TCP, note that this time the wrapped lambda is return function (n) {return n = = 0? 1:n*g (g) (n-1)}, so the 13th line is written} () (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 the introduce binding to add a parameter recursion_in_mind to the parameterless function and replace it with G (g), which, of course, requires g (g) to be passed in. Because Recursion_in_mind has a special meaning for this function, we use a special name that represents a hypothetical function that computes the factorial recursively. Its purpose is described in a later step.
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 * Recursi On_in_mind (n-1) } } (g (g)) (n) } } ) return FX (5)} ())
One more TCP, note that this time the package is the lambda represented by the entire FX. So the parentheses at the end of the 18 line are called.
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)} ())
One more time. Introduce Binding, the one that was added to the previous step 18 row passed in a parameter, and the parameter does not use a free variable, but instead uses a lambda directly, the code is a bit dilatory, and you can 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) } } ) C17/>return FX (5)} ())
Use a variable temp to refer to the previous step directly into the lambda, while the FX renamed to Y, yes this y is a form of y-combinator. And for the sake of code readability, multiple lines of code are sorted into one line. Deriving to this step, the code structure suddenly changed and not quite the same, you can see now there are two named functions exist, one is temp, the other is Y. The calculation process is clear two steps, the first step is fact = y (temp), which is based on the temp as raw material, Y for the production process, a fact function is produced, this step is in the manufacturing function. The second step uses 5来 to invoke the newly crafted function. The temp function is also very special, it receives a recursive function recursion_in_mind, and then calls this function to calculate the factorial of the n-1. Derivation to this step, it can be thought that Y-combinator has been deduced, but the standard form of y-combinator in the control lambda calculus will be found, now the appearance of Y is obviously inconsistent with it, so to further derivation.
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 derives a little bit of trouble because the previous refactoring 5 methods are not used, and need to introduce a new concept called "fixed point of the function", the fixed point of function f is a value x makes f (x) = x. For example, 0 and 1 are fixed points of function f (x) = x^2 (function for calculating squares), 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 the high-order function f is another function, G, which makes f (g) = G.
Back to the derivation, we now need a conclusion about the fixed point to complete the subsequent deduction, that is, temp (fact) = fact (can not consider why need such an intermediate conclusion, now the task is to prove the correctness of this conclusion, until the conclusion is used to know why it is needed), What does that mean? Applying the definition that has just been said, the fixed point of the high-order function temp is another function fact that makes temp (fact) = fact, as long as this step is equal to achieve. Subsequent derivation is no problem.
Looking back at 19 lines of return fact (5) You can tell that fact is a function that calculates the factorial of 5 independently. And what is temp, temp is a function that takes a function as a parameter and returns a function. Temp (fact) passes the fact into temp, which means that a function that calculates the factorial is passed in, and by looking at the function implementation of temp, temp just uses this factorial function to calculate the factorial of n-1. And then multiply N, and the return is just a function of calculating n!, and just as we have concluded from the return fact (5) that fact () is also a function of calculating n!, so we can draw this intermediate conclusion, that is, temp (fact) = fact. The intermediate conclusion is established!
According to the original plan, we should hurry up and take this hard-earned temp (FACT) = Fact conclusion continue on the road began to deduce, before the road to interrupt an interesting phenomenon, because this intermediate conclusion, fact is the function temp fixed point, And the function fact is the Y function of the raw material temp processing, so we can be defined from another angle for y-combinator, y-combinator can calculate a function (that is, the temp function) of the fixed point function (that is, fact).
Go on, look at the code in line 17 above to know that the fact function is the g (g) of 12 rows, because return g (g) is assigned to fact, and the Y function is called in 17 rows, and the input is temp, that is, the f in the Y function is temp. Take out the intermediate conclusion that just came out. Temp (FACT) = fact, we can replace the return G (g) of line 12th with return F (g), because fact is the fixed point of temp, so g (g) is the fixed point of F, so f (g) = = g (g ) is established. So this step deduction only modifies one line, that is, 12 rows above. At this point, this step refactoring is done.
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 final step, the return F (g (g)) of the above 11 lines is required for the function Wrap of the 5 refactoring method, the passed parameter is N, and with this modification, the current JavaScript version of the Y function is exactly the same as Y in the standard lambda calculus, very good. Again, against the way we gave it before derivation.
function wrap//Y is form y-combinatorconsole.log (function () { temp = function (recursion_in_mind) { Retu rn 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)) (N)}} ( func tion (g) {return function (n) {return F (g (g)) (n)}} ) } fact = Y (temp) return fact (5)} ())
At this point, the derivation of Y is over, but I don't think it's perfect, because a named variable temp,y is a recursive call to implement anonymous functions, we put the temp here what mean, male and female search not pro Ah, so good people do the bottom, We ended up using inline again, dropping the lambda represented by temp directly to the Y function, which is just perfect for demonstrating the true purpose of Y: to make a recursive call to its own function (the fact function) for an anonymous function, complete with the full text.
Inlineconsole.log (function () { //λg. ( ΛX.G (x x)) (ΛX.G (x x)) Y = function (f) { return function (g) {return function (n) {return F (g)) (N)}} ( func tion (g) {return function (n) {return F (g (g)) (n)}} ) } fact = Y ( function (recursion_in_mind) { Retu rn function (n) { return n = = 0? 1:n * recursion_in_mind (n-1)} } ) return to Fact (5)} ())
JavaScript derivation Y-combinator (from Jim Weirich)