λ演算(英语:lambda calculus,λ-calculus)是一套从数学逻辑中发展,以变量绑定和替换的规则,来研究函数如何抽象化定义、函数如何被应用以及递归的形式系统。它由数学家阿隆佐·邱奇在20世纪30年代首次发表。lambda演算作为一种广泛用途的计算模型,可以清晰地定义什么是一个可计算函数,而任何可计算函数都能以这种形式表达和求值,它能模拟单一磁带图灵机的计算过程;尽管如此,lambda演算强调的是变换规则的运用,而非实现它们的具体机器。
lambda演算可比拟是最根本的编程语言,它包括了一条变换规则(变量替换)和一条将函数抽象化定义的方式。因此普遍公认是一种更接近软件而非硬件的方式。对函数式编程语言造成很大影响,比如Lisp、ML语言和Haskell语言。在1936年邱奇利用λ演算给出了对于判定性问题(Entscheidungsproblem)的否定:关于两个lambda表达式是否等价的命题,无法由一个“通用的算法”判断,这是不可判定性能够证明的头一个问题,甚至还在停机问题之先。
lambda演算包括了建构lambda项,和对lambda项运行归约的操作。在最简单的lambda演算中,只使用以下的规则来建构lambda项:
产生了诸如:(λx.λy.(λz.(λx.zx)(λy.zy))(x y))的表达式。如果表达式是明确而没有歧义的,则括号可以省略。对于某些应用,其中可能包括了逻辑和数学的常量以及相关操作。
本文讨论的是邱奇的“无类型lambda演算”,此后,已经研究出来了一些有类型lambda演算。
λ演算是图灵完备的,也就是说,这是一个可以用于模拟任何图灵机的通用模型。 λ也被用在λ表达式和λ项中,用来表示将一个变量绑定在一个函数上。
λ演算可以是有类型或者无类型的,在有类型λ演算中(上文所述是无类型的),函数只能在参数类型和输入类型符合时被应用。有类型λ演算比无类型λ演算要弱——后者是这个条目的主要部分——因为有类型的λ运算能表达的比无类型λ演算少;与此同时,前者使得更多定理能被证明。例如,在简单类型λ演算中,运算总是能够停止,然而无类型λ演算中这是不一定的(因为停机问题)。目前有许多种有类型λ演算的一个原因是它们被期望能做到更多(做到某些以前的有类型λ演算做不到的)的同时又希望能用以证明更多定理。
λ演算在数学、哲学、语言学和计算机科学中都有许多应用。它在编程语言理论中占有重要地位,函数式编程实现了λ演算支持。λ演算在范畴论中也是一个研究热点。
作为对数学基础研究的一部分,数学家阿隆佐·邱奇在20世纪30年代提出了λ演算。 但最初的λ演算系统被证明是逻辑上不自洽的——在1935年Stephen Kleene和J. B. Rosser举出了Kleene-Rosser悖论(英语:Kleene–Rosser paradox)。
随后,在1936年邱奇把那个版本的关于计算的部分抽出独立发表—现在这被称为无类型λ演算。 在1940年,他创立了一个计算能力更弱但是逻辑上自洽的的系统,这被称为简单类型λ演算。
直到1960年,λ演算与编程语言的关系被确立了;在这之前它只是一个范式。由于Richard Montague和其他语言学家将λ演算应用于自然语言语法的研究,λ演算已经开始在语言学和计算机科学学界拥有一席之地。
在λ演算中,每个表达式都代表一个函数,这个函数有一个参数,并且会返回一个值。不论是参数和返回值,也都是一个单参的函数。可以这么说,λ演算中只有一种“类型”,那就是这种单参函数。函数是通过λ表达式匿名地定义的,这个表达式说明了此函数将对其参数进行什么操作。
例如,“加2”函数f(x)= x + 2可以用lambda演算表示为λx.x + 2(或者λy.y + 2,参数的取名无关紧要),而f(3)的值可以写作(λx.x + 2) 3。函数的应用(application)是左结合的:f x y =(f x) y。
考虑这么一个函数:它把一个函数作为参数,这个函数将被作用在3上:λf.f 3。如果把这个(用函数作参数的)函数作用于我们先前的“加2”函数上:(λf.f 3)(λx.x+2),则明显地,下述三个表达式:
是等价的。有两个参数的函数可以通过lambda演算这样表达:一个单一参数的函数,它的返回值又是一个单一参数的函数(参见柯里化)。例如,函数f(x, y) = x - y可以写作λx.λy.x - y。下述三个表达式:
也是等价的。然而这种lambda表达式之间的等价性,无法找到某个通用的函数来判定。
并非所有的lambda表达式都能被归约至上述那样的确定值,考虑
或
然后试图把第一个函数作用在它的参数上。(λx.x x)被称为ω 组合子,((λx.x x)(λx.x x))被称为Ω,而((λx.x x x) (λx.x x x))被称为Ω2,以此类推。若仅形式化函数作用的概念而不允许lambda表达式,就得到了组合子逻辑。
在数学和计算机科学中,“可计算的”函数是基础观念。对于所谓的可计算性,λ-演算提供了一个简单明确的语义,使计算的性质可以被形式化研究。λ-演算结合了两种简化方式,使得这个语义变得简单。第一种简化是不给予函数一个确定名称,而“匿名”地对待它们。例如,两数的平方和函数
可以用匿名的形式重新写为:
同样地,
可以用匿名的形式重新写为:
第二个简化是λ演算只使用单一个参数输入的函数。如果普通函数需要两个参数,例如的记法方式,表示在(“主程序”的另一个lambda项)中,要以来表示(一些明确的lambda项),则写成如下:
作者经常引入类似如let的语法糖,允许以更直观的次序撰写上述内容:
通过等号链接这个命名常量,即可将lambda演算“编程”的一个lambda项,写为零或多个函数的定义,而使用构成程序主体的那些函数。这个let显著的限制,是在中并没有定义名称,因为不在绑定的lambda抽象范畴之内;这意味着递归函数定义不能以let来使用。更进步的letrec语法糖允许以直觉的方式编写递归函数定义,而不需用到不动点组合子。
递归是使用函数自身的函数定义;在表面上,lambda演算不允许这样。但是这种印象是误解。考虑个例子,阶乘函数f(n)递归的定义为
在lambda演算中,你不能定义包含自身的函数。要避免这样,你可以开始于定义一个函数,这里叫g,它接受一个函数f作为参数并返回接受n作为参数的另一个函数:
函数g返回要么常量1,要么函数f对n-1的n次应用。使用ISZERO谓词,和上面描述的布尔和代数定义,函数g可以用lambda演算来定义。
但是,g自身仍然不是递归的;为了使用g来创建递归函数,作为参数传递给g的f函数必须有特殊的性质。也就是说,作为参数传递的f函数必须展开为调用带有一个参数的函数g -- 并且这个参数必须再次f函数!
换句话说,f必须展开为g(f)。这个到g的调用将接着展开为上面的阶乘函数并计算下至另一层递归。在这个展开中函数f将再次出现,并将被再次展开为g(f)并继续递归。这种函数,这里的f = g(f),叫做g的不动点,并且它可以在lambda演算中使用叫做悖论算子或不动点算子来实现,它被表示为Y -- Y组合子:
在lambda演算中,Y g是g的不动点,因为它展开为g(Y g)。现在,要完成我们对阶乘函数的递归调用,我们可以简单的调用 g(Y g)n,这里的n是我们要计算它的阶乘的数。
比如假定n = 5,它展开为:
等等,递归的求值算法的结构。所有递归定义的函数都可以看作某个其他适当的函数的不动点,因此,使用Y所有递归定义的函数都可以表达为lambda表达式。特别是,我们现在可以明晰的递归定义自然数的减法、乘法和比较谓词。
某一些lambda项有普遍接受的名称:
其中有几个在“消除lambda抽象”中有直接的应用,将lambda项变为组合演算的术语。
如果是一个没有λ-抽象的lambda项,但可能包含了命名常量(组合子),则存在一个lambda项(,),这相同于一个缺少λ-抽象(除了作为命名常量的一部分,如果这些被认为是非原子的)的λ.;也可以被视为匿名变量,就如同(,)从之中删除所有出现的,同时仍然允许在包含的位置替换参数值。
转换函数可由下式定义:
在这两种情况下,形式(,)可借由使初始的组合子I,K或S获取参数而化简,就像(λ.) 经过β-归约一样。I返回那个参数。K则将参数抛弃,就像(λ.),如果在中不是以自由变量出现。S将参数传递给应用程序的两个子句,然后将第一个结果应用到第二个的结果之上。
组合子B和C类似于S,但把参数传递给应用的一个子项(B传给“参数”子项,而C传给“函数”子项),如果子项中没有出现,则保存后续的K。与B和C相比,S组合子实际上混合了两个功能:重新排列参数,并复制一个参数,以便它可以在两个地方使用。W组合子只做后者,产生了SKI组合子演算的B,C,K,W系统。
自然数的函数F: N → N是可计算函数,当且仅当存在着一个lambda表达式f,使得对于N中的每对x, y都有F(x) = y当且仅当f x == y,这里的x和y分别是对应于x和y的邱奇数。这是定义可计算性的多种方式之一;关于其他方式和它们的等价者的讨论请参见邱奇-图灵论题。