λ演算(lambda演算)原理通俗易懂的详细总结

λ演算

中午我做了一个梦,梦里我写了一篇叫做“λ演算”的文章,我怕忘了所以一边做梦一边写了下来。所以下面的内容全部都是胡扯,一个字都别信。

总有文章会选择在开篇说一段废话

λ演算本身的内容是比较少的,它主要用来:一,精确表示一个函数,具体来说是可计算的函数——可以在有限的时间内执行完的函数;二,研究这种函数的性质。所以换句话来说,λ演算和图灵机(一个在纸带上读写符号、移动读写头位置的机器,跟电脑CPU本质上一样,执行简单指令的机器)等价。λ演算可以理解为将“计算”这一概念等同为字符的替换,是一套形式化的东西,和平时接触到的事物和使用的自然语言没有太直接的联系。所以就当λ演算是一种新的东西来认识就好了。

λ演算的基础概念

λ演算包含下面的几个基础概念:

  • λ项
  • 变量
  • 抽象
  • 应用

其中“λ项”是λ演算中的重要单位。它们的内容分别是:

  • 一个λ项是指一个变量、一个抽象或一个应用;
  • 一个变量是指由任意数量字符组成的字符串;
  • 一个抽象由一个变量和一个λ项组成,格式为“λ‘变量’.‘λ项’”,如“λx.x+1”;
  • 一个应用由间隔分开的两个λ项组成,如“M”和“N”都是λ项那么“M N”就是一个应用。

当一行λ演算由多个λ项组成时,为了防止歧义可以像一般的数学运算一样用括号把优先级高的部分括起来。比如:“(λx.x+1) (λv.v+m)”。

接下来我来具体介绍上面的几个基础概念。

变量

像一般的数学式子一样,x、y、z这些单字母符号可以作为λ演算中的变量使用,但是按照定义,多个字母组成的符号、加减符号或者其它的特殊符号都可以是变量。如“x+y”,当把它看作是普通的数学式子时,x和y是参数。但是在λ演算里它是字符“x”、“+”和“y”组成的字符串,所以“x+y”是一个变量,一个不可分割的整体。与此类似,加“+”、减“-”、乘“*”、除“/”这些符号在λ演算里它们只是普通的字符而已,作用是作为字符串的组成部分充当变量,它们不是λ演算里面原生的计算用符号。符号“λ”和“.”也可以作为λ演算中变量的符号,但是一般是不会这样用的,因为这两个符号在λ演算里有特殊作用——用来描述抽象。

λ演算中的变量只是用来区分,所以一般情况下为了方便和直观都用单个字符来做变量名。这时如果出现一个多字符组成的字符串,尤其是这个字符串是一个如“x+y”这样的数学式子时,这个字符串往往不是指一个普通的变量,而是指一个由复杂的内部细节组成的、功能是字符串里的式子所描述的字面意思的λ项。比如抽象“λx.x+1”,字符串“x+1”看成是实现了“x+1”功能的λ项,里面的“x”在需要的时候可以作为一个运算用的参数被替换。一般这种情况是定义抽象的时候出现在小数点“.”符号的后面。

抽象

抽象以符号“λ”开始,到第一个“.”之间的内容是变量,第一个“.”后面到最后的是一个λ项。例如“λx.x+1”是抽象,“λ”和“,”之间的“x”是变量,“x+1”应该理解为功能是把前面的λ符号后的变量“x”进行加一操作的λ项。

再比如“λx.λy.λz.x+y+z”是一个抽象,因为它以符号“λ”开始,第一个“λ”到第一个“.”之间的“x”是变量,第一个“.”后面的“λy.λz.x+y+z”是一个λ项。

因为变量是一个字符串就行,不一定是只有一个字符,所以严格按照定义的话字符串“x+y”或“xy”也可以是一个变量。因此“λxy.λx+y.x+y-xy”是一个符合规定的抽象。但是就像上面所说的一样,一般变量名都用单字母字符来表示,多字符组成的字符串变量往往潜在地假设它是一个更复杂的有内部细节的抽象。

注意“λx.x y”是一个抽象而不是一个应用,因为按照定义点“.”到行尾结束之间的“x y”属于抽象中的λ项部分。当需要表示这个是应用的时候,需要用括号隔开,“(λx.x) y”,这样“λx.x”优先看成一个单独的部分。

抽象是有实际含义的:符号“.”后面的λ项里面存在一个变量,可以被替换成别的东西。例如“λx.x-1”表示“x-1”这个λ项里有一个名称为“x”的变量,当处于应用左侧时可以被右边的λ项替换。可以简单地理解为“λx.x-1”这种写法和“f(x)=x-1”等价。

应用

两个λ项用一个间隔隔开,组成一个应用。比如“(λx.x+1) 2”,空格左边的“λx.x+1”是一个抽象,一个抽象是一个λ项,右边是一个“2”,“2”是变量,一个变量也是一个λ项,所以“(λx.x+1) 2”是两个λ项,所以这是一个应用。

空格的两边只要是λ项就行,比如“2 λx.x+1”严格来说是一个符合规定的应用。

一个多次嵌套的应用总是优先地将左边两个λ项作为一个应用看待的。如“x y z”,“x”和“y”组成一个应用,“x y”和“z”组成一个应用,如果用括号表示即:“(x y) z”,一般这种情况下省略括号。但是“y z”如果是一个应用,那么和左边的“x”一起就需要加上括号:“x (y z)”。

“应用”这一个概念的实际含义是:左边的λ项和右边的λ项发生(或者说是执行)名为“β-归约”的作用。而什么是“β-归约”会在下面的内容中详细说明。另外为了凸显这种相互作用中左右λ项的关系,可以用类似“将λ项a应用于λ项b”的说法来称呼应用“a b”。

λ项

一个变量是一个λ项,一个抽象也是一个λ项,一个应用也是一个λ项。上面的例子有涉及λ项的概念,这里再举一个复杂一点的例子。

如“(λx.λy.x+y) 1 2”,从左往右看,第一个出现的部分是抽象“λx.λy.x+y”,它作为一个λ项和空格后的λ项“1”(这里的“1”是一个变量,一个变量是一个λ项)组成了一个应用“(λx.λy.x+y) 1”,同时一个应用是一个λ项,这就和后面的“2”组成了又一个应用,即“((λx.λy.x+y) 1) 2”。

在λ演算里,所有操作的基本对象都是λ项,就好像面向对象编程里一切都是对象一样。但是和对象这种具有一定的内部状态并且把状态封装起来的状态机不一样的地方是,作为λ演算基本对象的λ项偏向于指某些具有一定的模式的执行过程或者说行为模式。不太正确但是简单地说,它的内部状态或者说内部的某种可在特定情况下改变的状态参数是通过类似函数的形式参数的方式由外部传递进去,并且它可以利用抽象(“λx.f”这种)来不断扩展而不是封装起来(自身的功能可以通过参数不断扩展)。相关内容将会在下面说到的λ演算的基础操作中说明。

λ演算的基础操作

α-变换

可以在不改变抽象所代表的含义的前提下,改变抽象中的变量(也就是“λ”和“.”之间的部分)的内容,这种操作称为“α-变换”。

例如“λx.x-1”经过α-变换可以变成“λy.y-1”。“λa.λb.a+b”可以变成“λx.λb.x+b”,进一步再变成“λx.λy.x+y”。但是只是替换“λx.λx.x”为“λy.λx.y”是不行的,因为原本“.”右侧的“λx.x”是一个只返回变量x本身的λ项,替换后右侧变成“λx.y”也就是返回一个和变量x毫无关联的东西,含义发生了改变。

α-变换可以用于解决变量同名冲突的问题,因为在以上的例子中没有涉及应用所以意义不明显。

所谓的变量同名冲突是指,我们现有的一个λ项里存在变量“x”,而我们要新添加一个λ项和现有的λ项组成一个更复杂的λ项,而这个新加入的λ项里面也有一个“x”,而且这个“x”实际上和我们现有的“x”不是指同一个东西。这时候如果不换一个变量,直接添加进来的话就会造成歧义。

下面说明β-归约时会提到α-变换的实际应用。

β-归约

β-归约是指将一个应用中出现的左右两个λ项,用替换的方式把右侧合并到左侧最终只剩下一个λ项的操作。β-归约是理解λ演算的一个重要的部分。

例如“(λx.x+2) 2”,左侧是一个抽象,表示“x+2”中的x可以被替换。应用右侧的λ项是“2”,那么用“2”替换得到“2+2”,β-归约结束。这里操作步骤是,去除左侧的符号“λ”和“.”前面的变量,将右侧的变量“2”直接换掉“x+2”中的“x”。

又例如,已知“λx.λy.x+y+2”,我们希望用另一个λ项“λx.x+3”和它组成一个应用:“(λx.λy.x+y+2) λx.x+3”。因为左边的“λx.λy.x+y+2”里面出现了“x”,右边也有“x”所以直接用“λx.x+3”的话会出现变量同名冲突,需要先对应用的左边(或右边)的λ项进行α-变换。如将左边的“x”换成“z”得到:“(λz.λy.z+y+2) λx.x+3”。然后进行β-归约,得到:“λy.λx.x+3+y+2”,β-归约结束。操作步骤是,首先同上一个例子直接去除“λz”,“z+y+2”替换成了“λx.x+3+y+2”,得到结果:“λy.λx.x+3+y+2”。那么如果左边是“λz.λy.y+2+z”呢?这时去除“λz”,然后看结果(为了明显一点我加个括号):“λy.y+2+(λx.x+3)”,这个结果的含义是:左侧表明这是一个抽象,变量是“y”,点右侧内容是“y+2+(λx.x+3)”,因为含有一个抽象“λx.x+3”所以“y+2+(λx.x+3)”是一个抽象而不是一个变量,所以应该写成“λx.y+2+x+3”才对,于是得到最后的结果“λy.λx.y+2+x+3”。

又例如,左侧是一个变量构成的λ项,这意味着无论右侧是什么,左侧都是返回变量本身。如:“2 λx.y”,左侧是变量“2”,没有任何可以替换的东西所以无论右侧是什么,β-归约结果都是“2”。

如果左侧是一个应用,如“(λx.x) (λy.y+1) 4”,左侧是“(λx.x) (λy.y+1)”右侧是“4”,需要先对左侧进行β-归约得到“(λy.y+1) 4”,再进行β-归约得到“4+1”。

总而言之,也可以这样理解β-归约:如果说λ演算里的“应用”的左侧λ项是一个函数,右侧λ项是一个实际参数,那么β-归约就是把函数应用到右侧的实际参数上,或者说β-归约是右侧的实际参数代入左侧的函数里。或更直白地说是给定一个实际参数然后往一个函数里面找到形式参数,将实际参数替换掉所有的形式参数。由于β-归约不仅会替换变量而且会整合过程中涉及的若干λ项,得到新的内容,所以β-归约是λ演算的一个重要的部分。

η-变换

η-变换是指两个不同的λ项如果各自经过α-变换、β-归约,最后能得到意义完全一样的λ项,那么这两个λ项就是等价的。由于这个内容理解起来很容易,所以这里就不仔细说明了。

使用方法和示例

使用λ演算定义自然数

λ演算中是没有自然数的概念的,只有λ项和组成λ项的变量、抽象和应用,所以在使用λ演算定义自然数当然是利用这几个东西来定义。下面给出0到5的定义:

0 = λf.λx.f
1 = λf.λx.f x
2 = λf.λx.f (f x)
3 = λf.λx.f (f (f x))
4 = λf.λx.f (f (f (f x)))
5 = λf.λx.f (f (f (f (f x))))

可以发现这样的规律:

定义 出现多少个应用 可以进行多少次β-归约
0 = λf.λx.x 0 0
1 = λf.λx.f x 1 1
2 = λf.λx.f (f x) 2 2
3 = λf.λx.f (f (f x)) 3 3
4 = λf.λx.f (f (f (f x))) 4 4
5 = λf.λx.f (f (f (f (f x)))) 5 5

除了从0到1之外,在上一个自然数基础上往“.”右边的应用的左侧添加一个“f”组成新的应用就能得到一个比原来的值大1的自然数。这种定义方式表达了以下的内容:

  • 自然数“0”类似于接受两个参数,然后返回第二个参数的函数。它对两个参数什么都不做。
  • 自然数“1”同理理解为返回了第一个参数应用于第二个参数的结果,这时第一个参数会被作为函数,传入第二个参数然后解析并执行一次(β-归约一次)。
  • 自然数“2”,是第二个参数代入第一个参数,结果(相当于自然数“1”)作为参数再次代入第一个参数后的结果。也就是第一个参数被代入两次,执行了两次。
  • 后续同理,第二个参数代入第一个参数结果再次代入第一个参数,第一个参数被代入多少次就代表自然数几。

简而言之,λ项“f”当作自然数计数的函数,它和它之前的β-归约结果再进行β-归约的次数即代表对应的自然数。

定义返回比给定自然数大1的自然数的函数

使用任意给定自然数“n”,返回比给定自然数大1的自然数,即函数“y=n+1”。假如用变量λ项“add”表示,是:

add = λn.λf.λx.f (n f x)

这里是基于上面的自然数定义来定义的,下面来看下是不是对的。往这个“函数”里面代入“1”,也就这里计算结果是返回“2”,过程如下:

1 + 1 = add (λf.λx.f x)
      = (λn.λf.λx.f (n f x)) (λf.λx.f x)
      = λf.λx.f ((λf.λx.f x) f x)
      = λf.λx.f ((λx.f x) x)
      = λf.λx.f (f x)

“λn.λf.λx.f (n f x)”左起第一个变量是“n”,左侧应用到右侧的“(λf.λx.f x)”(也就是上面的自然数定义“1”)时变量“n”被β-归约过程消除,得到“λf.λx.f ((λf.λx.f x) f x)”。然后继续进行β-归约,得到最后结果“λf.λx.f (f x)”。可以发现,这个结果和上面的自然数“2”的定义是一样的。

接下来给出“n=2”时的情况:

2 + 1 = add (λf.λx.f (f x))
      = (λn.λf.λx.f (n f x)) (λf.λx.f (f x))
      = λf.λx.f ((λf.λx.f (f x)) f x)
      = λf.λx.f ((λx.f (f x)) x)
      = λf.λx.f (f (f x))

显然,结果等于“3”,正确。最后试一下如果是“0”代入会怎样:

0 + 1 = add (λf.λx.x)
      = (λn.λf.λx.f (n f x)) (λf.λx.x)
      = λf.λx.f ((λf.λx.x) f x)
      = λf.λx.f ((λx.x) x)
      = λf.λx.f x

显然,结果等于“1”。

定义加法

还是基于上述的定义,定义λ项“+”为加法:

+ = λa.λb.λf.λx.a f (b f x)

这里的变量“a”和“b”表示需要代入加法“+”进行相加的两个自然数。试一下“1+1”:

1 + 1 = + (λf.λx.f x) (λf.λx.f x)
      = (λa.λb.λf.λx.a f (b f x)) (λf.λx.f x) (λf.λx.f x)
      = (λb.λf.λx.(λf.λx.f x) f (b f x)) (λf.λx.f x)
      = (λb.λf.λx.(λx.f x) (b f x)) (λf.λx.f x)
      = (λb.λf.λx.(f (b f x))) (λf.λx.f x)
      = λf.λx.f ((λf.λx.f x) f x)
      = λf.λx.f ((λx.f x) x)
      = λf.λx.f (f x)

结果“λf.λx.f (f x)”正是上面定义的自然数“2”。

参考材料

  • [学习] 从 函数式编程 到 lambda演算 到 函数的本质 到 组合子逻辑
  • 组合子逻辑、λ演算的历史背景和产生动机
  • CHURCH 计数和 LAMBDA 演算
  • 函数式编程的λ演算
  • 可计算性理论模型初识(一):巴斯克范式(BNF)与λ演算
  • Computational calculi primer Part 1: Lambda calculus & BNF 这是上一篇文章的原文
  • 函数式编程λ项的约简
  • λ演算

你可能感兴趣的