In the last "digression" article, we introduced how to realize the concept of a natural number through a few simple concepts. This also tells us that, in fact, one of the core content of functional programming is to use the least concept to derive more concepts to achieve functions. This Java , requires a lot of native concepts, and then more derived concepts are needed to solve the problem.

However, in fact, we have always avoided talking about it in the above article. In fact, we have used the concept of "recursion". Is this concept a necessary "native" concept? It is also a "derived" concept that can be replaced by the "native" concept. In this chapter, we are trying to solve this problem.

Lambda calculus

First of all, we have to return to the definition of \(\lambda\) calculus, that is, as a functional programming, at least one \(\lambda\) calculus must be implemented (of course there are more complex combinator models, which are not considered here for the time being ):

  1. Variable: \(x\)
  2. Abstract: \(\lambda x. M\)
  3. Application: \((MN)\)

Equivalent to functional programming, the most basic is to realize these three concepts. In Python , these three manifestations are the variable , anonymous function , anonymous function with the value . For example, in the following example, x is a variable, f = and g = are anonymous functions/abstractions, and f(1) and g(lambda x: x + 1, 1) are applications:

f = lambda x: x + 1
f(1)

g = lambda h, x: h(x)
g(lambda x: x + 1, 1)

But in fact, \(\lambda\) is actually not enough, because the \(\lambda\) calculus only provides a set of symbol substitution logic , such as the above f and g examples. In fact, we pass \(\lambda\) calculus can only find:

>>> f(1)
1 + 1

>>> g(lambda x: x + 1, 1)
(lambda x: x + 1)(1)
(1 + 1)

Therefore, the first thing to add is the logic of simplification/calculation, which involves the logic of four arithmetic operations and Boolean operations. Of course, these can also be derived from the concept of symbols/functions, as we did in the previous chapter. Same.

Let's look at what we need for a recursive function. Here we implement a function with an exponent

power_of_2 = lambda x: 1 if (x == 0) else 2 * power_of_2(x - 1)

We first discovered that for a function that can work in real life, "branch" is necessary, and it will terminate unnecessary infinite loops.

In fact, we found a problem that power_of_2 front again not fully implemented, it has been called power_of_2 , which requires us to have a compiler requires special handling, you can ignore it. And we found that the above formula cannot be expressed by \(\lambda\) calculus. This presents a problem. The concept of "recursion" must be natively used as part of the programming language, or we can use the $$\lambda\) calculus to achieve it. us back to the concept of 1616e42cb73ee2 Y combinator that we are going to discuss in this article.

Y combinator

We re-look at this issue, in fact, it is easy to solve, is to power_of_2 this also as variables lambda pass, that it should be of the form:

# 注意这里是伪代码不可执行
power_of_2 = lambda f: ???? lambda x: 1 if (x == 0) else 2 * f(x - 1)

In fact, one thing we have to do is how to implement the application can be replaced in a loop and iterate itself indefinitely. Similar to this:

$$ f(g) = g(f(g)) = g(g(f(g))) = ... $$

Or the symbolic expression of \(\lambda\) calculus is

$$ (\lambda x. N) g = g((\lambda x. N) g) $$

We only need to find the symbol set \(N\) that conforms to this substitution. The famous Haskell Curry found one such answer, namely \(Y = \lambda f.(\lambda x. f(xx))(\lambda x. f(xx))\). Generally we call this Y Y combinator . We can check the above formula.

$$ \begin{align} Y g && = && \lambda f.(\lambda x. f(x x))(\lambda x. f(x x)) g\\&& = && (\lambda x. g(x x))(\lambda x. g(x x)) \\&& = && g((\lambda x.g(x x))(\lambda x. g(x x))) \\&& = && g(Y g) \end {align} $$

Note: I need to explain the derivation process here. The second line actually quotes \(g\) to \(Y\); the third line applies \((\lambda x. g(xx)) \) to \((\lambda x. g( xx))\).

Python version of Y by ourselves according to the above formula:

Y = lambda f: (lambda g: f(g(g)))(lambda g: f(lambda y: g(g)(y)))

We realize power_of_2 now only need a statement def

power_of_2 = Y(lambda f: lambda x: 1 if (x == 0) else 2 * f(x - 1))

Of course, if we encounter a multivariate function, we may need a generalization of the Y combinator-the Z combinator:

Z = lambda f: (lambda g: f(g(g)))(lambda g: f(lambda *y: g(g)(*y)))

For example, the following function power find the exponent:

power = Z(lambda f: lambda x, n: 1 if (n == 0) else x * f(n - 1))
power(2, 3)

To sum up, so far, we have realized the concept of "recursion" only through the concept of \(\lambda\) calculus by means of Y combinators. Therefore, it can be argued that the concept of "recursion" can be "derived" in functional programming languages without the need for "native" support. We only need \(\lambda\) calculus to simulate the concepts of natural numbers, operations, and recursion. Of course, this article is just an introduction to let you see the expressive ability and theoretical foundation of functional programming, as well as the connection with mathematics, especially abstract algebra, and symbolic logic. Later we will slowly witness how mathematical concepts can be transformed into applicable examples.


三次方根
1.2k 声望101 粉丝