【fixed point】λ演算
原文地址: 神奇的λ演算
现在时兴讲函数式编程,弄得如果不会写两句λ表达式你都不好意思跟人说自己是敲代码的。所以我也就趁着这阵风头,琢磨琢磨了这个函数式编程。怎么算来,也有个三年两载了,出师还不敢说,将将入门估计还算凑合。朋友说让给写篇文章或者翻译翻译哪个大佬的文章,盛情难却,就把刚学的这点水倒出来出出糗好了。
说函数式编程这个东西,根源来自于λ演算,虽然后面加了很多特性,但总归要从这里出发。关于λ演算这个东西,也是很有历史的,有兴趣了解相关历史的,可以参阅相关的wikipedia,这里就不多说了。咱们直奔主题。
引子
说到 λ 演算,自然要从 λ 表达式说起。到底什么是 λ 表达式呢?其实很简单,就是一种特殊的语法所书写的匿名函数。
所谓匿名函数,就是说我们不需要给这个函数起个什么名字。这一点很重要,如果按照非匿名函数的观点,但凡是函数都需要有个名字,那么就很难和“函数是第一类对象”这样的观点兼容。毕竟,动态生成的无穷多种可能性的函数,我们总不能预先都给他们准备好名字是吧。更何况,就算有这个心思,也没这个可能,光一阶的函数的数量就已经高于整个实数的数量了,比实数少的自然数我们都列不完呢(虽说叫可列集,不过谁都没列完过),更何况是它了。
我们来看看一般目前的语言怎么去写匿名函数。其实这里有点倒因为果,毕竟是先有了λ演算,才有了现代语言中的这些匿名函数。不过便于理解起见,也就不管那么多了。先来看看JavaScript、Matlab和C#几种语言的具体实现方式
// javascript
var f = function (x) { return x+1; }
% Matlab
f = (x) x + 1;
// C#
Function<int, int> f = x => x + 1;
看,写起来都还算简单,不过数学家们有更偷懒的办法,他们把上面的函数用更加简化的方式来表示,只用一个关键字 'λ' 来表示对函数的抽象。还是上面这个函数,用不太规范的方式来表示下 λ 演算中对上述函数表达方式,大体上是这样
λ x. x+1
所谓的不规范的地方,主要就是这个加法运算,具体怎么不规范了,请看下节。
基本概念
学界有个公认的观点,叫做“奥卡姆剃刀”原则,说如果有多种理论去解释相同的现象,假设最少的那个往往是最受欢迎的。
学程序设计的同好们都知道,学一门编程语言,我们总是要去了解这门语言中诸如数据类型、变量、常量、运算符、表达式这些原生定义的东西。这种东西就相当于上帝之手,设定了最基本的规则和假定,然后我们在它画的这个圈圈里面玩。
哲学上一个常识,内涵越小外延越大。我们搞了那么多的假定,往往就给自己做了很大的束缚。数学家不喜欢这么玩,所以构建这个λ演算的时候,更倾向于用尽可能少的原生性规则来进行限定。
于是,他们规定了一下最基本,最核心的三个基本假定,我们不妨理解为最基本的语法规则。
-
第一个就是所谓函数为第一类对象的原则,用文字陈述有点绕口,这么说的:“ 全体λ表达式构成Λ空间,λ表达式为Λ空间到Λ空间的函数 ”。
我曾经尝试过用C#来表达这句话,居然通过编译了,贡献出来// C# delegate L L(L x);这说的是,定义了一个委托类型L,这种委托类型以一个类型为L的参数作为输入,返回一个L类型的返回值。所有L类型的实例,就构成了上述的Λ空间。
-
第二个是函数的抽象原则,即对于一个λ表达式,不妨称为P,我们可以用一个字母代表哑元,不妨称为 x,使用抽象规则,我们可以构造出另一个λ表达式来,像这样
λ x. P它表示我们构造了一个函数,这个函数以一个λ表达式输入,返回另一个λ表达式,这个返回值等于将P中所有非哑元的字母 x 用刚刚输入的那个表达式替换掉。
例如我们有几个表达式,P分别为x y z y (λ x. x)如果应用上述抽象规则,并用 x 标记新抽象的哑元,可以得到
λ x. x y z λ x. y (λ x. x)如果应用某个不等于也不包含 x, y, z 字母的表达式 a 作为输入的话,上述两个λ表达式计算的结果应当分别为
a y z y (λ x. x)为什么第二个里面的 x 仍然保留呢,就因为它是在 P 中的哑元,其实和P外的哑元是没关系的。可以用一个避免混淆的方式给它改改名字,比如说改成
λ x. y (λ w.w)其实表达的是同一个意思,就好像这两个代码其实表达的运算是一个意思一样
// javascript var f = function (x) { return x + 1; }; var g = function (y) { return y + 1; };哑元的更换,不影响 λ 表达式的所表达的含义。
-
第三个规则比较直观,就是应用函数的规则,对于一个 λ 表达式 P 如果希望将另一个 λ 表达式 q 作为它的输入进行求值,我们简单的将他们左右并列书写即可
P q这里不使用括号写法P(q),因为我们后面可以看到,如果用上括号,我们会后悔的。注意到一个 λ 表达式应用一个 λ 表达式作为输入后,返回的还是一个 λ 表达式,它即可以作为函数再接受一个新的输入,比如说 r,就可以写成这样
P q r这里就默认了左结合优先的自然的规则。它也可以作为输入,传递到另一个 λ 表达式中,比如说 S,就可以写成这样
S (P q)因为前面提到了左结合优先的原则,所以这里这个括号就不可避免了。
这三个规则,就构成了λ演算的全部基石,有的作者用更加形式化的方式将他们整理起来,有的还甚至提炼出更加具体的一些法则,其实我觉得,从本质上讲,这三者其实是核心,外围的那些规则都是一些自然的辅助性衍生。如果希望看比较规范严谨的定义的,我推荐H. Simmons和A. Schalk写的这本书,他们写的非常严谨,相当的棒。如果希望看稍微通俗一点,又不像我这样太通俗的,建议看看R. Rojas写的这本,只有13页。如果还嫌长,看A. Jung的这篇10页的小文也好。其实我都是从wikipedia上转摘来的,这几篇我看过觉得不错,更完整的书单还是建议去Lambda Calculus的页面看,毕竟萝卜青菜各有所爱嘛。
Bool运算
如果前面那些算看起来比较枯燥,让人看了有然并卵的感觉的话,那现在我们就算正式踏上了 λ 演算的神奇之旅了。我们先来看看最简单的情况,用上述定义的基本规则,我们来构造Bool运算。
首先是True和False,简写作 T 和 F,我们采用Church的方案(此大咖就是这整个神奇 的 λ 演算的发明人,我没看他的原文,就不厚着脸皮转引他的文章了)
T = λ x y. x
F = λ x y. y
这里,使用了著名的柯里化技巧(Currying),用一元函数构造多元函数,展开来写 T 的定义应当是
T = λ x. (λ y. x)
表示的含义是 T 是这样一个函数,它接收任何一个输入 x,并会返回一个函数 λ y. x,这个被返回的函数接收另外一个输入 y,而不论这个输入 y 是什么,它返回第一个输入的参数 x .
同样,F 是这样一个函数,它的展开式写作
F = λ x. (λ y. y)
它接收 x 并返回 λ y. y,而后者接收什么就原模原样的返回什么。
这里 T 和 F 接受第一个参数后所返回的两个函数也很重要,可以分别称为常函数 λ y. x 和恒等函数 I = λ y. y。其实更常见的恒等函数写作 λ x. x,上一节我们提到过,变换哑元不改变函数含义。
站在柯里化技巧的基础上,我们可以重新将 T 理解为一个选择函数,它接收两个参数,而返回第一个参数。而 F 则相反,它返回第二个参数。
T a b = (λ x. (λ y. x)) a b = (λ y. a) b = a
F a b = (λ x. (λ y. y)) a b = (λ y. y) b = b
在不影响理解的情况下,将重复书写的λ和不必要的括号都省略掉,就可以简略的书写柯里化技巧下的多元函数
T = λ x. (λ y. x) = λ x. λ y. x = λ x y. x
F = λ x. (λ y. y) = λ x. λ y. y = λ x y. y
就是这一节我们前面所提到的 T 和 F 的定义。
不动点
在数学上,我们经常会遇到不动点的概念。所谓不动点,即对某个函数 f(x) 存在这样的一个输入 x,使得函数的输出仍旧等于输入的 x 。形式化的表示即为
f(x) = x
比如说刚刚学过循环小数的小朋友们就很喜欢纠结这个问题
0.999... = 1
实际上这个可以用不动点的方式去理解,我们可以设 x = 0.999...,观察到 x 扩大到原来的10倍的时候再减去9,得到的仍旧是 x 本身,因此 x 是函数
f(x) = 10x - 9
的不动点,满足方程
x = f(x) = 10x - 9
简单的求解一下方程,就可以得到 x = 1的结论。
在前面的讨论中,我们曾接触过恒等算子 I = λ x. x ,对它而言任何一个 λ 表达式都是它的不动点。而对于一般性的 λ 表达式,寻找它对应的不动点,在后面的讨论中我们可以看到,是一个非常有意义的事情。
对于一般性的数学函数而言,不动点的存在性并不是一定的,比如说指数函数 ex,它就不存在不动点。而对于存在不动点的函数,寻找其不动点往往也是需要一定技巧的。但是那仅仅是对于一般意义的数学函数而言,在 λ 演算体系下,Λ空间中的每一个 λ 表达式都具有一个不动点。这来源于下面将要介绍的著名的不动点定理。
在介绍不动点定理之前,我们先来看一个比较有趣的 λ 表达式
ω = λ x. x x
它表示接受一个输入 x,返回 x 作用到 x 自身的结果。比如说
ω a = a a
ω I = I I = I
现在要耍点赖皮了,我们让 ω 自己作用到自己身上,并按照应用函数的法则将其展开
ω ω = (λ x. x x) ω = ω ω
展开后竟然又还原回来了,这似乎和我们所说的不动点的重现机制很近似。
而事实上,几个比较著名的不动点算子的构造过程都借助了这种技巧。下面我们来看最著名的不动点算子 Y 组合子
Y = λ f. (λ x. f (x x)) (λ x. f (x x))
这里应用了两次自身作用自身的技巧,第一个是显式出现的 x x,而后者是整体上出现的
a = λ x. f (x x)
Y = λ f. a a
给出了 Y 组合子,不动点定理其实就已经通过构造法得到了证明。不动点定理如此说: 对于任意 λ 表达式 g,总存在不动点 x = Y g,使关系 g x = x 成立。简写作
g (Y g) = Y g
证明过程很简单
Y g = (λ f. (λ x. f (x x)) (λ x. f (x x))) g
= (λ x. g (x x)) (λ x. g (x x)) # (a)
= g ( (λ x. g (x x)) (λ x. g (x x)) ) # (b)
= g Y g # 将 (b) 中 (a) 重现的部分重新写作 Y g
除了 Y 组合子之外,其它比较著名的不动点算子还有
X = λ f. (λ x. x x) (λ x. f (x x))
Θ = (λ x y. y (x x y)) (λ x y. y (x x y))
浙公网安备 33010602011771号