魂断不动点——Y组合子的前世今生
序言
关于Y组合子和λ-演算相关的介绍资料实在太多了,但它们似乎都不太完全。既然Y组合子的全称为Y不动点组合子,那么它至少包含了不动点和组合子两种属性。本文作为一个综合性的论述,试图回答关于Y组合子的几个问题,包括:
- 组合子是什么?
- 引入组合子的动机是什么?
- 不动点是什么?
- Y组合子是如何推导出来的?
- 不动点组合子是唯一的么?
- 如何在 Call-By-Value 的语义中实现Y组合子?
需要说明的是,为了让我们的讨论更加严谨,我们非常小心地用形式化方法自习地定义了朴素λ-演算,造成的结果是整片文章充斥了大量的数学定义与推导。读者不必深究于其中的数学定义,只需要关注核心思想即可。
初识不动点
定义函数 f:R↦R,如果 ∃x∈D(f),使得 x=f(x),那么我们称点 x 是函数 f(x) 的不动点。
容易发现,根据等式 x=f(x),我们可以进行下面这样的无穷代换:
初等函数的不动点非常好求。我们只需要将将等式 x=f(x) 稍作移项处理,就可以得到方程 f(x)−x=0,这里我们就把寻找不动点的问题归约为了方程求根问题。
λ-演算简介
λ-演算旨在考察函数的计算性质,它是将函数看作变量的更名代换规则而不是看作图像的一种不带类型的理论模型。它最初由 Alonzo Church 和他的学生 Stephen Cole Kleene 在20世纪30年代引入。最开始,Church 试图创造一套完整的形式系统作为数学的基础,当他随后发现这个系统易受罗素悖论的影响。虽然如此,λ-演算中涉及整数的那部分是完全成功的。利用这个理论 Church 将“可计算问题”形式化地定义为“λ-可定义”。Kleene 在随后的研究中,证明了λ-可定义等价于 Godel - Herbran 的递归性。
需要指出的是,λ-演算的内核十分之小,朴素λ-演算(Naive λ-Calculus)或称无类型λ-演算(Type-Free λ-Calculus)可以用下面的文法来描述:
在有的文献中式[5]也被称作原子(Atom),因为它们都是最小而不可再细分的元素,我们可以很容易理解它们的“意思”。式[6]被称为抽象规则(Abstraction Rule),它是λ-演算中构造函数的唯一方式。式[7]被称为应用规则(Application Rule),它是抽象规则的逆过程,也是复合函数的实质计算过程。然而,应用规则的语义并不像我们看上去的那么“显然”,我们将在后面仔细考察这个问题。
这里需要注意一下,式[7]的记法不同于习见的 S-表达式或 M-表达式的记法,我们采用的记法是在执行“应用”操作的函数两边加括号,这样不但使“应用”操作变成右结合的(Right Associative),同时也可以避免大部分情况下括号的密集出现:
按照我们的记法,式[2∼4]可以改写为:
下面将考察λ-演算的语义。首先我们需要定义全等(Identical)这个概念,我们可以按照下面的方式归纳地定义全等,其中 P 和 Q 都是任意的λ-表达式:
显然,两个全等的表达式,其语义是完全相同的。下面考虑易名规则(Renaming Rule),记号是 {φ/ψ}P ,表示将 P 中所有出现 ψ 的地方都替换为 φ ,其中 φ∈⟨identifier⟩ 且 ψ∈⟨identifier⟩ 。其形式化定义如下,其中,P,Q都是任意的λ-表达式:
为了考察应用规则的语义,我们还需要先考察自由变量的定义,记号 ϕ(E) 表示λ-表达式 E 中所有自由变量组成的集合,它可以按照下面的方式归纳定义:
我们说 ψ 在λ-表达式 P 中自由出现,当且仅当 ψ∈ϕ(P) ,此时称 ψ 为自由变量(Free Variable),否则称为绑定变量(Bounded Variable)或约束变量。
例如,在表达式 (λx.λy.(x)y)λx.(y)x 中,变量 x 和变量 x 都是约束变量,虽然二者都是同一个字母,但并不是同一个约束变量,因为它们被不同的λ-抽象绑定着,有着各自的辖域(已经用颜色标记出来)。而变量 y 是自由变量,而 y 是约束变量,因为它被 λy 所绑定。所以 ϕ((λx.λy.(x)y)λx.(y)x)=y
理解λ-演算中变量绑定的辖域非常重要。在定义清楚自由变量和绑定变量以后,我们开始介绍α-变换(α-conversion)。
α-变换有个很直观的解释:绑定变量的名字不重要。这在习见程序设计语言中被解释为:局部变量只是占位符,外部对其名字不敏感。但是需要注意α-变换同时也强调不能将绑定变量更名为子表达式中出现过的符号,否则就会将子表达式中的自由变量意外地变成约束变量,或者提升了约束变量的约束辖域。
为了定义β-归约,我们需要先要引入代换规则(Substitution Rule)。请读者注意代换规则与易名规则一个显著的不同:易名规则只允许用原子符号去替换原子符号,而代换规则允许用复合λ-表达式去替换原子符号。为了同易名规则区别开,我们使用记号 [P/φ]Q 表示用λ-表达式 P 代换 Q 中所有自由出现的 φ ,其中 φ∈ϕ(Q) ,P,Q 是任意的λ-表达式。
代换规则中的式[23]还使用到了易名规则,下面给出一个具体的例子来说明这条规则。
∵x∈ϕ(λy.(x)y)且y∈ϕ(λx.(y)x)
∴引入新变量z,显然z≢x≢y
注意到代换规则保证自由变量带入λ-表达式后仍然是自由变量,且约束变量的辖域不会改变。读者可以尝试不使用易名规则引入新变量,直接将表达式带入,再验证新表达式每个变量的辖域。
下面正式引入β-归约(β-reduction),其中 P,Q 是任意的λ-表达式:
我们将形如 (λφ.P)Q 的λ-表达式称为β-归约式(β-redex) ,将 Q/φ]P 的值称为归约结果(contractum)。直观地看,归约结果应该比归约式要简单,比如 ((λx.λy.(x)y)(λx.x))x→x。但实际上有很多β-归约式它们的归约结果并不比原表达式简单,式[25∼27]演示了一个特殊的β-归约式——它的归约结果就是它自己。
不难发现,如果一个归约式的归约结果比它本身“简单”,那么可以期望这个归约过程在某个地方能够停止下来,以至于无法再应用β-归约让它变得“更简单”。在这个信念下,我们引入范式(Normal Form)这个概念,其中 φ∈⟨identifier⟩ ,且P,Q为任意的λ-表达式。
没有无穷归约的λ-表达式一定有范式,反之不尽然。下面给出一个虽然有无穷归约的子表达式,但最终仍可归约为范式的λ-表达式[†]来结束本小节的讨论。
[†] 需要 Church-Rosser 定理来改换β-归约的顺序。
逻辑组合子
我们在上一节强调过,在λ-演算中,区分变量是绑定的还是自由的十分重要。λ-表达式的值可能依赖于其中的自由变量,而自由变量的值则依赖于上下文(Context)。非正式地,我们可以把上下文想做是一个有很多“洞(Hole)”的λ-表达式,我们可以向这些洞中填入任意的λ-表达式。式[28]就是一个可以作为上下文使用的λ-表达式:
我们尝试将λ-表达式 λx.(y)x 带入这个上下文,这就意味着我们要代换掉 C 中的 hole。
通过简单的β-归约,我们可以很容易地观察到,表达式 λx.(y)x 在这样的上下文 C 中的语义变成了 λx.(F)x ,显然不同的上下文中会有不同的 F ,这也就导致了λ-表达式的语义变化。自由变量好比习见程序设计语言中的全局变量,而绑定变量则像函数的形式参数。在程序设计语言中,通常提倡尽量少用甚至不用全局变量,都是为了规避不可控的上下文对局部过程的影响。
组合子逻辑最初由 Moses Ilyich Schönfinkel 于1924年发现。Schönfinkel 认为自由变量只是在语法上起辅助作用的概念,于是为了消除数理逻辑中对变量的需要,他引入了组合子逻辑。在大约1926-1927年间, Haskell B.Curry 开始研究代换过程,通过引入跟 Schönfinkel 相同的组合子,他将代换分解为了跟细小的步骤。在1927年晚期, Curry 发现了 Schönfinkel 的研究,并称其“早已预见了我的工作”。尽管如此, Curry 并没有放弃他对组合子逻辑的研究。他游学至德国哥廷根(Gottingen),在 Bernays 指导下完成了博士论文。在论文中,他构建了一套组合子的形式化系统,并证明了组合子 B,C,K,W 是完备的。
在组合子逻辑中,有两个特殊的组合子(也称基本组合子) S 和 K,它们的定义如下:
Curry 试图使我们相信,组合子逻辑是完备的,通过 S 和 K 两个标准组合子的组合构造,我们可以构造出其它的函数——这就是它们被称为“组合子”的原因。例如,λ-演算中的恒等函数 λx.x 可以构造为 ((S)K)K,我们可以根据β-归约很容易地验证两者的等价性:
不动点Y组合子
请读者考虑阶乘函数 FACT 的定义:
其中 ZERO? 和 PRED 都是组合子,其具体定义请参见[附录A]。部分读者可能不太适应用这种方式编码 FACT 函数,这是因为通过库里化(Currying)来实现多参函数调用,并将 if 控制流编码为组合子的方式在传统程序设计语言中并不常见。下面给出与它们等价的 Scheme 代码帮助读者理解,读者可以不用深究 FACT 的定义,只需关注不动点的推导过程。需要强调的是,传递给 PRED 的参数是邱奇数组合子,此处读者可以略过这些操作上的细节。
回到式[29],其中,等式左边是β-归约式,我们要将函数给“抽象”出来,可以通过向等式右边添加 λ 项得到。因此,我们有:
观察等式右边,我们发现仍然存在自由变量 FACT,我们需要再次使用λ将其抽象出来。(有读者会认为此处的 ZERO? 和 PRED 组合子也是自由变量。这种想法是错误的。这里的 ZERO? 和 PRED 只是一种简写记号。它并不依赖于任何上下文,读者可以将[附录A]中 ZERO? 和 PRED 的定义代换到这个式子中即可。)
我们可以将式[31]看做如下表达式:
在式[32]中,F 是 FACT,E 是组合子 λf.λn.(((ZERO?)n)1)((∗)n)(f)(PRED)n 。考虑式[32]中的等式,可以无穷地将这个等式替换等式右边出现的 F,即:
请将式[34∼36]与式[10∼12]比较,我们不难发现,F 也是某个函数的不动点。这里的“某个函数”,就是指组合子 E 。但是此时我们并没有解出 F ,并且与初等函数不同,我们还没有介绍“解高阶函数方程”的方法。幸运的是,类似于一元二次方程,我们有解高阶函数不动点的“求根公式”——传说中的Y不动点组合子。既然Y组合子是“通用求根公式”,那么它必然满足下面的等式:
同时也要注意到 F=(E)F 这个事实,于是我们有:
推导Y组合子的表达式是一项非常有创造力的工作。我们发现式[25∼27]的模式很像我们的Y组合子,但它总是保持自己不变,而Y组合子会不断增加一个前缀。于是,我们可以在式[25∼27]的基础上稍作修改,从而得到Y组合子的表达式:
于是,我们就能利用“万能求根公式”Y组合子来计算函数 FACT的定义了。注意,这个时候解出的 FACT 也是一个组合子,因为它的表达式中没有自由变量:
一个事实:有无穷多个不动点组合子
我们给出了Y不动点组合子,实际上还有一个非常出名的不动点组合子,即T组合子—— Turing 不动点组合子,它的定义如下:
我们发现,Y组合子和T组合子是非常地相像,以至于我们可以发现它们之间满足下面的等式:
事实上,我们可以通过下面的方式构造一个由无穷个不动点组合子序列:
其中,Y2组合子即是T组合子,而G组合子它的定义如下:
Curry悖论:无法逃离的自指怪圈
证:如果 a∈A ,那么我们就记作 (A)a。
那么对于集合 $\{x | P[x]\},我们可以表示为\lambda x. P[x]$。 |
这样,$ a \in \{ x | P[x]\} 就变成了(\lambda x. P[x])a = P[a]$。 |
取 $R = \{ x | x \not \in x \} = \lambda x. (\neg)(x)x$ 。 |
于是有 ∀r.[(R)r⟺(¬)(r)r]
于是有 (R)R⟺(¬)(R)r
注意到 (R)R≅(λx.(¬)(x)x)λx.(¬)(x)x=(Y)¬
证:逻辑算符蕴含满足下面的公理:
同时逻辑蕴含也要满足肯定前件(modus ponens)规则,即:
我们将逻辑蕴含 → 编码成我们系统中的组合子 imp ,因此公理[Γ1]可以编码为:
对任意的λ-表达式 Q,我们命:
又命:
即 P 是 N 的一个不动点。因此,我们得到:
在公理[Γ1]中应用式[55]定义的等式,我们得到:
式[58]作为公理的代换结果,其值也因永真。因此对任意的 Q (注意 N 的定义依赖于 Q),我们有
对式[59]使用[modus ponens],我们得到:
与我们前面得到的 Q 为任意的λ-表达式相悖。
这个悖论不难理解。考虑到 P→Q⟺¬P∨Q 的事实,逻辑连词 → 实际包含了 ¬ 的语义。既然在上面的讨论中,我们证明了 ¬ 在λ-演算中会导致矛盾,在一个蕴含 ¬ 的系统中导出系统不一致就不足为奇了。
在 Call-By-Value 语义语言中实现Y组合子
参考文献
- [Abel83] Abelson H, Sussman G J. Structure and interpretation of computer programs[J]. 1983.
- [Baren84] Barendregt H P. The lambda calculus[M]. Amsterdam: North-Holland, 1984.
- [Hank04] Hankin C. An introduction to lambda calculi for computer scientists[M]. King’s College Publications, 2004.
- [Hind06] Cardone F, Hindley J R. History of lambda-calculus and combinatory logic[J]. Handbook of the History of Logic, 2006, 5: 723-817.
- [Klee81] Kleene S C. The theory of recursive functions, approaching its centennial[J]. Bulletin of the american mathematical society, 1981, 5(1): 43-61.
- [RevG09] Révész G E. Lambda-calculus, combinators and functional programming[M]. Cambridge University Press, 2009.
附录A 常见组合子的定义
在组合子逻辑中,虽然 S,K 组合子足以构建出一个完备的系统,但通常也引入组合子 I 来简化某些表达式的表示,故组合子逻辑有时也称为SKI逻辑。
同时,Curry 还引入了两个组合子 B,C:
需要提及一些历史变迁。B,C,I,K,S 都是基本组合子的现代版本,在 Schonfinkel 最初的版本中,它们分别被称作 Z,T,I,C,S 。
布尔值以及 if-then-else 控制流也能被编码为组合子:
细心的读者可以发现,在组合子逻辑和λ-演算中 K 组合子和布尔值 true 都是采用的相同编码。实际上,Lisp 过程 car
也可以采用这样的定义。同时,布尔值 false 和随后将要介绍的邱奇数 0 跟 Lisp 中的 cdr
是相同的编码。
在我们的系统中,能将 if-then-else 定义为组合子,乍看之下有点奇怪,但考虑到 true≅car 以及 false≅cdr ,if 的定义就能很容易地推导出来。
由于式[62]中含有自由变量,我们用三个λ-项将自由变量抽象出来,这样就得到了式[61]中 if 的定义了。
在组合子逻辑中,邱奇数(Church Numerals)很好地展示了通过合适地编码,逻辑系统也可以拥有算术性质:
直观地来说,自然数 n 在组合子逻辑中被编码为一个将首参数在次参数上应用n次的函数。邱奇数可以使用下面的组合子来执行计算:
PRED 组合子乍看之下十分复杂,它的发现也颇具意味。Church 本人并没有找到 PRED 的定义,他试图使自己相信 PRED 组合子不是λ-可定义的。然而 Kleene 却发现了这个组合子[Klee81]。