在lambda 演算中,所有的计算都可以归结为函数定义和应用,它在语言功能的规约、语言设计与实现以及类型系统的研究中有广泛应用。它的重要性来自于既可以视作简单的程序语言,又可以看作一个能够严格证明的数学对象。
lambda演算仅仅是core calculi中的一个,除此之外pi-caculus可以用来定义基于消息的并发语言语义;object calculus蒸馏了OO语言的核心功能。
lambda calculus可以通过不同方式来进行扩充。首先,添加特殊的具体语法如numbers,tuples,records等很方便,它们的行为可以在core language中模拟。甚至还可以添加更复杂的特性如可变引用等。正如我们将看到的,对core language的扩展也经常牵扯到类型系统的扩展。
5.1 Basics
过程式(或者函数式)的抽象是所有语言的核心功能。不用一遍遍写重复的计算过程,我们只需要写一个通用的过程或者函数,之后实例化这个函数,为每个参数提供值。
这里我们给出一个阶乘的定义:
本质上,在lambda演算中,所有事情都是函数。它的语法只包含三类terms。
下面的小节探讨了定义的细节。
抽象和具体语法
当讨论程序语言的语法时,我们需要区分两类结构:
- 具体语法: 程序员直接读和写的字符串
- 抽象语法:程序作为标记树
从具体语法到抽象语法的转化过程,一般分为两个阶段:
- 词法分析
- parser:将一系列tokens转为抽象语法树
这本书的重点是抽象语法,我们一般需要在心里认为所写的term实际上是一棵抽象语法树。
为了少写括号,我们采用了两个习惯:
- 函数应用是左结合的,例如s t u表示 (s t) u
- 函数抽象的内部要尽可能向右扩展,例如λx. λy. x y x -> λx. (λy. ((x y) x))
变量和元变量
t表示任意term;而x表示任意变量。
Scope
我们必须处理的最后一个问题是变量的作用域(Scope)。变量x称为bound,当它出现在函数体t中。我们可以说lambda x是一个binder,它的作用域是t。变量x称为free,当它没有被一个x上的抽象绑定。
一个没有free variables的term称为封闭的。closed terms也称为combinators(组合子)。最简单的组合子,是恒等函数,
id = lambda x.x;
操作语义
在纯粹的lambda演算中,并不存在内置的常量或者原始的算子–没有numbers,算术操作,条件,records,循环,I/O等。唯一的terms计算的方式就是函数的应用。计算的每一步如下表示:
我们称(lambda x.t12) t2为redex,并把上述rewrite redex的操作称为beta reduction。存在一些不同的求值规则:
- full beta-reduction: 任何redex都可以在任何时候退化。
- normal oder:最外层的首先被规约。
- call by name: 不允许抽象内部进行规约
- call by value: 只有最外层的redex被规约,并且它的右边应该已经被规约成一个值,注意到这里唯一的值其实只有函数抽象。
注意到这个策略是严格的,无论函数参数是否被用到,它都必须先求值。
求值策略的选择事实上不会对类型系统的讨论产生影响。在本书中,我们使用call by value。
5.2 Programming in Lambda-Calculus
这一节我们将看一些使用lambda calculus编程的例子。
Multiple Arguments
我们可以观察到lambda calculus并没有提供内置的对于多参数函数的支持。这里我们可以使用high-order函数达到相同的目的。这种转换也称为currying。
f = λx.λy.s --> f v w
Church Booleans
我们可以在lambda演算中编码布尔值。可以定义tru和fls,如下:
这里我们想定义一个组合子test,具有性质test b v w规约为v当b为tru时;规约为w当b为fls时。
这里 test b v w 实际上规约为b v w。
我们接着定义and操作:
or = lambda b. lambda c. b tru c
not = lambda b. b fls tru
Pairs
Church Numerals
我们在这里可以定义后继函数:
其实就是继续应用一次s。我们进一步定义plus:
可以认为这里的s其实不变,但是z我们可以认为并不是从0开始。对于乘法:
我们可以认为s变成plus n;应用了m次。
我们可以定义iszero函数:
减法相比于加法来说很难构造,它将使用下面的技巧“前驱函数”,给c0做参数返回c0;给ci+1 返回ci。
扩展演算
在λNB中,我们对于布尔值以及数字有两种不同的实现,我们可以通过如下方法进行转换:
我们也可以将church numeral转为对应原始数字:
我们不能直接将succ应用到m上,这是因为我们定义的succ必须需要应用到一个表达式上。
注意到如果我们直接应用 scc c1,那么实际上如果采用call by value,我们只能得到一个抽象,而不是c2。主要的问题是我们很难检查这个值是否与我们的预期一致。一个比较直接的方法直接应用 realnat (times c2 c2)
Recursion
我们称没有normal form的term是diverge的。
Omega组合子有一个有用的泛化形式,称为不动点(fixed-point)组合子,可以用来定义递归函数:
我们定义阶乘如下:
Representation
我们可以认为在实际的数字和Church-numeral表示之间对于程序的运行结果并不具有较大差异。
5.3 Formalities
对于本章的剩余部分,我们更细节地考虑lambda演算的语法以及操作语义。
Syntax
我们可以定义一个term的自由变量。
Substitution
我们将使用两种不同的定义,第一种,我们将在本节引入,是紧凑和直观的;第二种我们将在第6章引入,记号上更加繁琐,具名变量将被数值索引替代。
首先让我们尝试一下可能的递归定义:
注意到如果选择bound变量,那么这个定义将不满足。例如,
很明显,我们在naive定义中犯的第一个错误是我们没有区分free和bound变量。下面是第二个尝试,
注意到这里仍会出现问题,
我们将一个常函数变为了恒等函数!
我们利用以下convention:
只有bound变量名字不同的terms我们认为是同一个term。例如,我们想计算[x -> y z] (lambda y. x y)我们可以首先rewrite (lambda y. x y) -> lambda w. x w。
操作语义
lambda演算的操作语义总结为下图:
有一个计算规则【E-APPABS】,以及两个一致规则【E-APP1, E-APP2】