The original text comes from the series of Good Math/Bad Math . The full text is divided into 7 chapters. This is the first chapter . The Chinese blog Negative Xuan Xiaohua translated the first 6 chapters of this series. OCD said that "there is no more below", so I made a complete set by myself. Only the original text is translated here, and the version of "Negative Xuan Xiaohua" adds a lot of stories to make it more interesting to read.
Good Math, Bad Math | Lambda calculus series 7 |
(In the original version of this post, I tried to use a JavaScript tool to generate MathML. But it didn’t go well: a few browsers couldn’t render correctly, and it didn’t display well in the RSS feed. So I had to start from scratch. , Rewrite it in a simple text format.)
My favorite Lambda calculus-opening
Computer science, especially programming languages, often tends to use a specific calculus: Lambda Calculus (Lambda Calculus). This kind of calculus is also widely used by logicians to study the nature of the structure of calculations and discrete mathematics. There are many reasons why Lambda calculus is great, including:
- very simple.
- Turing is complete.
- Easy to read and write.
- The semantics are strong enough to make (arbitrary) inferences from it.
- It has a good physical model.
- It is easy to create variants so that we can explore various properties of constructing computational or semantic ways.
Lambda calculus is easy to read and write, which is very important. It has led people to develop many excellent programming languages, which are based on Lambda calculus to varying degrees: LISP, ML and Haskell languages are extremely dependent on Lambda calculus.
Lambda calculus is based on the concept of functions. In pure Lambda calculus, everything is a function, and there is no concept of value. However, we can use functions to build anything we need. Remember in the early days of this blog, I talked about some methods on how to build mathematics? We can use Lambda calculus to build the entire structure of mathematics from scratch.
Without further ado, let's take a closer look at LC (Lambda Calculus). For a calculus, two things need to be defined: grammar, which describes how to write legal expressions in calculus; and a set of rules that allow you to manipulate expressions symbolically.
Grammar of Lambda Calculus
Lambda calculus has only three types of expressions:
- Function definition: The function in Lambda calculus is an expression, written as:,
lambda x . body
which means "ax
function whose parameter parameter is , and its return value isbody
the calculation result." At this time, we say: Lambda expression binds parametersx
. - Identifier reference: An identifier reference is a name that is used to match a parameter name in a function expression.
- Function application: Function application is written in the form of putting the function value in front of its parameters, such as
(lambda x . plus x x) y
.
Currying
There is a trick in Lambda calculus: if you look at the above definition, you will find that a function (Lambda expression) only accepts one parameter. This seems to be a big limitation-how can you achieve addition with only one parameter?
There is no problem at all, because functions are values. You can write a function with only one parameter, and this function returns a function with one parameter, so that you can write a function with two parameters—essentially the two are the same. This is called Currying, named after the great logician Haskell Curry.
For example, we want to write a function to achieve x + y
. We are more accustomed to write lambda x y . plus x y
something like: something like this. The way to write a function with a single parameter is: we write a function with only one parameter and let it return another function with only one parameter. So x + y
it becomes a one-parameter x
function, which returns another function, which will be x
added to its own parameters:
lambda x. ( lambda y. plus x y )
Now we know that the function that adds multiple parameters does not really add anything, but simplifies the syntax, so when I continue to introduce it, I will use the multi-parameter function when it is convenient.
Free Identifier vs. Binding Identifier
There is an important grammatical issue that I haven't mentioned: closure or complete binding. When evaluating a Lambda calculus expression, you cannot quote any unbound identifiers. If an identifier is a parameter of a closed Lambda expression, we say that the identifier is (by) bound; if an identifier is not bound in any closed context, then it is called a free variable.
lambda x . plus x y
: In this expression,y
andplus
are free because they are not any arguments Lambda expressions are closed; andx
is binding, because it is a closed-form expressions defined functionplus x y
parameters.lambda x y . y x
: In this expressionx
andy
are bound, because they are all parameters in the function definition.lambda y . (lambda x . plus x y)
: In the inner calculuslambda x . plus x y
iny
andplus
is free,x
it is binding. In the complete expression, thex
sumy
is bound:x
bound by the inner layer, and boundy
by the rest of the calculus.plus
Still free.
We will often use it free(x)
to represent x
free identifiers in expressions .
A Lambda calculus expression is completely valid only when all its variables are bound. However, when we leave the context and focus on the sub-expression of a complex expression, free variables are allowed-at this time it is very important to know which variables in the sub-expression are free.
Lambda calculus algorithm
Lambda calculus has only two real laws: called Alpha and Beta. Alpha is also called "conversion", and Beta is also called "protocol".
Alpha conversion
Alpha is a renaming operation; basically, the name of the variable is not important: given any expression in the Lambda calculus, we can modify the name of the function parameter, as long as we modify all the free references to it in the function body at the same time .
So - for example, if there is an expression like this:
lambda x . if (= x 0) then 1 else x ^ 2
We can use Alpha conversion to x
become y
(writing alpha[x / y]
), so we have:
lambda y . if (= y 0) then 1 else y ^ 2
This will not change the meaning of the expression at all. However, as we will see later, this is important because it allows us to implement things like recursion.
Beta Protocol
The Beta Statute is wonderful: this rule enables Lambda calculus to perform any calculation that can be done by a machine.
Beta basically means that if you have a function application, you can replace the part of the function body that is related to the corresponding function identifier. The replacement method is to replace the identifier with the parameter value. This sounds confusing, but it is very easy to use.
Suppose we have a function application expression: " (lambda x . x + 1) 3
". The so-called Beta protocol is that we can x + 1
implement function application by replacing the function body (ie " "), and 3
replace the referenced parameter" x
" with the value " ". So the result of the Beta Statute is " 3 + 1
".
A slightly more complex example: (lambda y . (lambda x . x + y)) q
. This is an interesting expression, because the result of applying this Lambda expression is another Lambda expression: that is, it is a function that creates a function. In the Beta protocol at this time, it is necessary to q
replace all reference parameters " y
" with the identifier " ". So, the result is " lambda x . x + q
".
Give another example that makes you even more uncomfortable: " (lambda x y. x y) (lambda z . z * z) 3
". This is a function with two parameters, and it (its function is) applies the first parameter to the second parameter. When we calculate, we replace the parameter " x
" in the body of the first function with " lambda z . z * z
"; then we 3
replace the parameter" y
" with" "to get:" (lambda z . z * z) 3
". Then execute the Beta Statute, there is " 3 * 3
".
The formalization of the Beta rule is:
lambda x . B e = B[x := e] if free(e) subset free(B[x := e])
The last condition " if free(e) subset free(B[x := e])
" explains why we need Alpha conversion: we can do the Beta protocol only if it does not cause any conflict between the bound identifier and the free identifier: if the identifier " z
" is in " e
" Is free, then we need to make sure that the Beta protocol will not cause " z
" to become bound. If there is a naming conflict between B
the variable bound in " e
" and the free variable in " ", we need to use Alpha conversion to change the identifier name to make it different.
The example can make this clearer: suppose we have a function expression, " lambda z . (lambda x . x + z)
", now, suppose we want to apply it:
(lambda z . (lambda x . x + z)) (x + 2)
The parameter " (x + 2)
" x
is free. Now, suppose we don’t follow the rules and do the Beta statute directly. We will get:
lambda x . x + x + 2
x + 2
Variables that were previously free in " " are now bound. Suppose we apply the function again:
(lambda x . x + x + 2) 3
Through the Beta protocol, we will get " 3 + 3 + 2
".
What if we first adopt Alpha conversion the way it should be?
- Made
alpha[x/y]
are:(lambda z . (lambda y . y + z)) (x + 2)
- Statute by Beta:
(lambda y . y + x + 2) 3
- Then the statute of
3 + x + 2
Beta: .
" 3 + x + 2
" And " 3 + 3 + 2
" are very different results!
The rules are pretty much that. There is another rule, you can optionally add a rule called Eta-Statute, but we will skip it. I describe here a Turing complete-a complete and effective computing system. To make it useful, or to see how it can be used to do something meaningful, we also need to define a bunch of basic functions that allow us to do mathematical calculations, conditional tests, recursion, etc., which I will discuss in the next article These ones.
We have not yet defined the Lambda-calculus model. (The original author discussed the concept of the model here and here .) The model is actually very important! Logicians came up with a complete model after fiddling with LC for several years. This is a very important thing, because although LC seems to be correct, in the early attempts to define a model for it, It failed. After all, remember that without an effective model, this means that the results of the system are meaningless!
Alonzo Church's genius work-numbers in lambda calculus
So, now, let's do something interesting with lambda calculus. First of all, for the sake of convenience, I will introduce some syntactic sugar to name functions so that we can read them when we encounter some complicated things.
Introduce the "global" function (that is, in all the introductions about lambda calculus that I have written, it can be used directly, instead of declaring this function in every expression), we will use the "let" expression :
let square = lambda x . x ^ 2
This expression declares a function named "square", and its definition is lambda x . x ^ 2
. If we have " square 4
", the equivalent expression of the above "let" expression is:
(lambda square . square 4) (lambda x . x ^ 2)
In some examples, I used numbers and arithmetic operations. But numbers don't really exist in lambda calculus, we only have functions! Therefore, we need to invent some way of using functions to create numbers. Fortunately, Alonzo Church, the genius who invented the lambda calculus, figured out a way to do this. His functionalized version of numbers is called Church Numerals.
All Church numbers are functions with two parameters:
- 0 is "
lambda s z . z
". - 1 is "
lambda s z . s z
". - 2 is "
lambda s z . s (s z)
- For any number "
n
", its Church number is a function of applying its first parameter to the second parameter"n
"times.
A good way to understand is to use " z
" as the name for the zero value, and " s
" as the name of the subsequent function. Therefore, 0 is a function that only returns a value of "0"; 1 is the function that applies the successor function to 0 last time; 2 is the function that applies the successor function to the successor of zero, and so on.
Now that we are optimistic, if we want to do addition, x + y
we need to write a function with four parameters; two numbers that need to be added; and the " s
" and " z
" used when deriving numbers :
let add = lambda s z x y . x s (y s z)
Let's currify it and see what's going on. First, it accepts two parameters, which are the two values we need to add; second, it needs to normalize these two parameters so that they both use the binding of 0 ( z
) and subsequent values ( s
) set (i.e., the parameters are written s
, and z
in the form of a combination).
let add = lambda x y . (lambda s z . (x s (y s z)))
Look at this formula, it says that in order to x
and y
added first with the parameter " s
" and " z
create (regularized) Church number" " y
." Then apply it x
to the Church number y
, this time use the Church number defined by " s
" and " z
" y
. In other words, the result we get is a function that adds itself to another number. (To calculate x + y
, to calculate y
is z
the successor to a few numbers, then the calculation x
is y
the successor to a few numbers.)
Let us take a closer look at 2 + 3
the calculation process:
add (lambda s z . s (s z)) (lambda s z . s (s (s z))) news newz
To make it easier to understand, do alpha transformation to the numbers 2 and 3. Replace "2" with "s2" and "z2", and replace 3 with "s3" and "z3":
add (lambda s2 z2 . s2 (s2 z2)) (lambda s3 z3 . s3 (s3 (s3 z3)))
add
Replace with the definition:
(lambda x y .(lambda s z. (x s y s z))) (lambda s2 z2 . s2 (s2 z2)) (lambda s3 z3 . s3 (s3 (s3 z3)))
To add
do the beta statute:
lambda s z . (lambda s2 z2 . s2 (s2 z2)) s (lambda s3 z3 . s3 (s3 (s3 z3)) s z)
Then the beta reduction of the Church number "3". This step is actually "regularization" 3: Replace the successor function and zero function in the definition of number 3 with the successor function and zero function in add
the parameter list:
lambda s z . (lambda s2 z2 . s2 (s2 z2)) s (s (s (s z)))
Now, it's the most subtle step. Then do a beta reduction for the Church number "2". We know: 2 is a function, it accepts two parameters: a successor function and 0 (function). So, to add 2 and 3, we use the successor function to apply to the first parameter of 2; the operation result of 3 is applied to the second parameter (function 0)!
lambda s z . s (s (s (s (s z))))
So, our result is: Church number "5"!
Boolean value and choice in Lambda calculus
Now that we have introduced numbers in lambda calculus, we can express any calculation with only two things: one is how to express choice (branch), and the other is how to express repetition. In this article, I will discuss Boolean values and selection, and the next article will introduce repetition and recursion.
We want to be able to write if / then / else
expressions in the form of sentences, just like we do in most programming languages. After representing numbers as functions like Church numbers, we also represent true and false values as functions that perform an if-then-else operation on its parameters:
let TRUE = lambda x y . x
let FALSE = lambda x y . y
So, now we can write a " if
" function, its first parameter is a conditional expression, the second parameter is an expression that is calculated if the condition is true, and the third parameter is if the condition is false The operation to be performed.
let IfThenElse = lambda cond true_expr false_expr . cond true_expr false_expr
In addition, we also need to define common logical operations:
let BoolAnd = lambda x y . x y FALSE
let BoolOr = lambda x y. x TRUE y
let BoolNot = lambda x . x FALSE TRUE
Now, let us go through these definitions. Let's take a look first BoolAnd
:
BoolAnd TRUE FALSE
, Expand TRUE and FALSE definitions:BoolAnd (lambda x y . x) (lambda x y . y)
- Alpha transform true and false:
BoolAnd (lambda xt yt . xt) (lambda xf yf . yf)
- Now, expand BoolAnd:
(lambda x y. x y FALSE) (lambda xt yt . xt) (lambda xf yf . yf)
- Beta Statute:
(lambda xt yt.xt) (lambda xf yf. yf) FALSE
- Beta statute again:
(lambda xf xf . yf)
So we get the BoolAnd TRUE FALSE = FALSE
result: . Let's take a look again BoolAnd FALSE TRUE
:
BoolAnd (lambda x y . y) (lambda x y .x)
- alpha transformation:
BoolAnd (lambda xf yf . yf) (lambda xt yt . xt)
- Expand BoolAnd:
(lambda x y .x y FALSE) (lambda xf yf . yf) (lambda xt yt . xt)
- Beta Statute:
(lambda xf yf . yf) (lambda xt yt . xt) FALSE
- Then beta statute:
FALSE
and so,BoolAnd FALSE TRUE = FALSE
Finally, let us do the calculations BoolAnd TRUE TRUE
:
- Expand two TRUE:
BoolAnd (lambda x y . x) (lambda x y . x)
- alpha transformation:
BoolAnd (lambda xa ya . xa) (lambda xb yb . xb)
- Expand BoolAnd:
(lambda x y . x y FALSE) (lambda xa ya . xa) (lambda xb yb . xb)
- Beta Statute:
(lambda xa ya . xa) (lambda xb yb . xb) FALSE
- Beta Statute:
(lambda xb yb .xb)
and so,BoolAnd TRUE TRUE = TRUE