lambda演算的荣光

来源:互联网 发布:telnet使用的端口 编辑:程序博客网 时间:2024/04/27 21:29

函数式编程背后的可计算性理论——λ演算的荣光

 

宏伟计划

十九世纪七十年代,康托尔创立了集合论,为现代数学打下了基础,也是产生危机的直接来源。希尔伯特在1900年于巴黎发表演讲《数学问题》,提出了23个数学家们需要解决的问题。然而悖论的出现使得整个数学大厦出现了动摇。其中最为严重的,是“罗素悖论”,其中涉及的是以自己为元素的集合,这就被称作“第三次数学危机”。后来这个定义被公理所排斥,危机得以解决。

于是在20世纪20年代,在集合论不断发展的基础上,大数学家希尔伯特向全世界的数学家抛出了一个宏伟计划——建立一组公理体系,使一切数学命题原则上都可由此经有限步推定真伪,这叫做公里体系的完备性。希尔伯特还要求公理体系保持“独立性”(所有公理都是相互独立的,使公理系统尽可能简洁)和“无矛盾性”(即相容性,不能从公理系统导出矛盾)。这又叫做希尔伯特判定问题,即能否有一个算法自动判定谓词逻辑表达式是真还是假。

希尔伯特将公理进行了彻底的形式化,并且他的计划也确实有了一定的进展。几乎全世界的数学家都乐观地看着数学大厦即将竣工,然而一声晴天霹雳,哥德尔使得这座数学大厦变成了一个海市蜃楼。他证明了:任何无矛盾的公理体系,只要包含初等算术的陈述,则必定存在一个不可判定命题,用这组公理不能判定其真假。即是说,“无矛盾”和“完备”是不能同时满足的,这便是著名的哥德尔不完备性定理。

为了证明不完备性定理,哥德尔写出了第一个“计算机程序”(用逻辑公式去形式化几个步骤)来验证一个命题是否能被证明。然而人们从哥德尔的“程序”中察觉到希尔伯特判定问题也可能不存在答案,或者没有这样的算法或者不可计算。但是要想证明这一点,首要的是定义清楚什么是有效可计算性。

1936年,计算机科学之父图灵发表了一篇划时代的论文《论可计算数及其在判定问题上的应用》,他用图灵机来定义可计算性,并用图灵机重写了1931年哥德尔的“程序”,证明了希尔伯特判定问题是不可计算的。

停机问题

一个很简单的例子,就是著名的停机问题——不存在这样一个程序(算法),它能够计算任何程序(算法)在给定输入上是否会结束(停机)。

假设存在这样一个图灵机,它能够判断任何程序在任何输入上是否停机。由于所有图灵机构成的集合是一个可列集,我们可以自然地列出一个矩阵,它表示一个图灵机Mi在每一个输入Tj下的输出,用N表示无法停机,其余数值表示输出

      T1  T2  T3  T4  …

M1  1    2    N    N    …

M2  N    3    N    3    …

假设判定停机问题的图灵机为H,它能够判断Mi在Tj下的输入是否停机,即H(i,j)是否为真。

现在构造一个新的图灵机P,运用康托尔的对角线方法,使得让P在Ti上的输入和Mi在Ti上的输入不一样,利用图灵机H这个新的图灵机P不难构造出来:

P(i):

If (H(i,i)==1)

      Return1+Mi(i)

Else

      Return0;

这样做我们确保了P(i)跟每一个Mi(i)都不一样。

然而,P本身也是一个图灵机,所以一定有一个整数k,Mk=P,两个图灵机等价,即对于所有的输入,Mk(i)=P(i)。

于是我们得到Mk(k)=P(k)=Mk(k)+1(Mk(k)停机)或者Mk(k)=0(不停机)。矛盾在于如果停机,则Mk(k)将没有一个确定的值,如果不停机,返回值为0说明它还是停机的。这表明不存在这样的P,也就不存在这样的H。从而得出停机问题是不可判定的。

λ演算的诞生

虽然图灵提出了图灵机这个解决可计算性问题的概念,但是他并不是第一个解决这个问题的人。早在1932年,阿隆佐邱奇提出了λ算子,其核心思想是“万物皆可为函数”。邱奇给出了一组无类型的函数定义规则,然后又加入了几条规则,试图以函数来形式化整个逻辑系统。但他的学生克林发现这套逻辑系统不一致,导致邱奇只发表了λ算子的内容。克林发现这个λ算子能够表示整数算术系统,这似乎是可计算性的定义。但是在他跟哥德尔提出这个想法的时候,遭到了哥德尔的反对。克林向哥德尔下了战书——如果这个定义不对,请提出你的定义,我能证明二者等价。1934年,哥德尔提出一般递归函数的概念,于是1936年克林将一般递归函数具体化,并证明了和他之前的定义等价。随后邱奇又证明一般递归函数和λ算子等价,并用λ算子证明了希尔伯特判定问题是不可计算的,最早地证明了这个问题。几个月后,图灵才发表了图灵机的概念。随后图灵跟随邱奇学习,并证明一般递归函数、λ演算和图灵机三者等价。于是才有了邱奇图灵论题。

邱奇图灵问题指出,所有的计算或算法都可以由一台图灵机来执行。以任何常规编程语言编写的计算机程序都可以翻译成一台图灵机,反之任何一台图灵机也都可以翻译成大部分编程语言大程序。该论题和以下说法等价:常规的编程语言可以足够有效的来表达任何算法。该论题被普遍假定为真。

λ演算和图灵机都可以用来支持可计算性理论,只不过图灵机更偏向于物理意义上真正的计算机,而λ演算则是一系列的按一定规则而进行的转换,更偏向于数学意义。

λ演算系统的形式化定义

 

λ演算系统中合法的字符如下:

1.   x1,x2,x3,…变元(变元的数量是无穷的,不能再有限步骤内穷举)

2.   à 归约

3.   = 等价

4.   ‘λ’‘(’‘)’

所有在λ演算系统中合法的字符只有上列4种,其他符号都是非法的。假如没有对‘+’的说明,那么λx.x+2是不合法的λ表达式。

λ-项

λ-项又称为λ表达式,它是由合法字符组成的表达式,合法的组成规则如下:

1.   任一变元是一个项

2.   若M,N是项,则(MN)也是项

3.   若M是一个项,x是一个变元,则(λx.M)是一个项

4.   仅仅由以上规则归纳定义得到的字符串是项

用BNF范式表达的上下文无关文法描述就是:

1.   <表达式> ::= <标识符>

2.   <表达式> ::=(λ<标识符> .<表达式>)

3.   <表达式> ::=(<表达式> <表达式>)

说明:λ-项是左结合的,所以f x y = (f x )y。诸如(λx.M)这样的λ-项被称为函数抽象,原因是因为它常常是一个函数的定义,函数的参数是变量x,函数体是M,函数名称则是匿名的。MN这样的项被称为函数应用,原因是它表达了将M这个函数应用到N这个概念。假如f(x)=x+1,那么可以被抽象表达为λx.x+1,而如果要表达f(2)的话,就要用形如MN的项(λx.x+1) 2。但是并非所有MN形式的项都是如此,比如xy也是MN形式的项,它跟函数理论谈不上直观联系,只是这个λ-项是合法的。因此,λ-项的形式化定义中,有一些是可以和函数理论直观联系,而另一些只是合法,没有直观的字面意义。

 

公式

若M,N是λ-项,则MàN,M=N,是公式。

变量自由出现法则

在一个λ-项中,变元要么自由出现,要么被一个λ符号约束。

考虑如下直观的表达方式,f(x)=xy,在这里面x就是约束变元,而y是自由变元。

最直观的理解,被λ符号约束的变元就是作为某个函数形参的变元,而自由变元则是不作为任何函数形参的变元。

自由出现法则:

1.   在表达式x中,如果x是一个变元,那么x就是一个单独的自由出现。

2.   在表达式λx.M中,自由出现就是M中所有除了变元x的自由出现。M中所有的x变元都是被λ符号约束的约束变元。

3.   在表达式(MN)中,变元的自由出现就是M和N中所有变元的自由出现。

用另一种方法表达,看起来会更加直接

1.   free(x) = x

2.   free(λx.M) =free(M) – {x}

3.   free(MN) =free(M) ∪ free(N)

λ x的辖域

假设有两个λ-项,λx.xx和λx.(xx),x的出现情况又是怎样的?

由于λ-项是左结合的,所以第一个λ-项可以写作是(λx.x) x,因此第一个x是约束变元,第二个x是自由变元。

而对于第二个λ-项,里面所有的x都是被λ约束了,这表明所有的x都是约束变元。

理解辖域的概念可以参考在谓词逻辑中量词的辖域。

 

归约

 

在λ表达式的集合上定义了一个等价关系,“两个表达式其实表示的是同一个函数”这样的直觉性判断即由此表述,这种等价关系是通过所谓的“α-变换规则”和“beta-归约规则”。

α-变换(α-conversion)

α变换规则所表达的是,约束变元的名称是不重要的。两个λ表达式λx.x和λy.y表达的是同一个函数,因此,将某个表达式中的约束变元全部换名是可以的,但是变换需要遵循规则。

如果M,N都是λ-项,x在M中有自由出现,若以N置换M中所有x的自由出现,可以得到另一个λ-项,记为M[x/N]

则变换规则如下:

λx.M=λy.M[x/y] 如果y没有在M中自由出现,那么y替换了M中的x,y不会被M中的λ符号约束

例子:λx. ( λx.x) x= λy. (λx.x) y

被替换的x只能是M中的自由变元,因此括号内的x没有被替换。

β-归约

β-归约表达的是函数应用或者函数代入的概念。MN是合法的λ-项,那么MN的含义是将M应用到N,通俗的说法是将N作为参数代替M中的约束变元传入M函数体。尽管N也可能是一个函数,它依然可以作为参数传入一个函数。理解函数作为参数传入的概念可以参考微分算子d/dx,它接收一个函数作为参数,并且返回值也是一个函数——参数函数的导数。

β-归约的规则如下:

(λx.M) N àM[x/N] (如果N中所有变量的自由出现都在M[x/N]中保持自由出现)

β-归约是λ演算中重要的概念和规则,它是函数代入概念的形式化表示

η-变换

η-变换表达的是外延性的概念,这里外延性指的是,两个函数对于所有的参数得到的结果一致,当且仅当它们是同一个函数。η-变换可以令λx.f x和f相互转换,只要x不是f中的自由出现。

外延性指出,如果两个函数f和g,如果f(x)=g(x),对于所有x都成立,那么两个函数f和g被认为是相等的。

若外延性是有效的,那么由β-归约,λx.f x和f之间可以相互转换,具体转换规则如下

对于所有的y,如果有(λx.f x) y = f y,那么λx.f x = f

 

λ演算的公理

1.   (λx.M)NàM[x/N] N中所有变量的自由出现都在M[x/N]中保持自由出现

2.   MàM

3.   MàN,NàL => MàL

4.   MàM’ => ZMàZM’

5.   MàM’ => MZàM’Z

6.   MàM’ => λx.Màλx.M’

7.   MàM’ => M=M’

8.   M=M’ =>M’=M

9.    M=N,N=L=>M=L

10.   M=M’ => ZM = ZM’

11.  M=M’ => MZ = M’Z

12.  M=M’ =>λx. M =λx. M’

如果某一公式MàN或者M=N可以用以上公理推出,则记为λ├MàN和λ├M=N

范式

如果一个λ-项M中,不含任何形为((λx.N1)N2)的子项,则称M是一个范式,简记为n.f.。如果一个λ-项通过有穷步β-归约后,得到一个范式,则称M有n.f.,没有n.f.的λ-项称为n.n.f.。

将一个λ-项进行β-归约,如果通过有穷步代入可以得到一个不能够代入的λ-项,那么这个λ-项就是它的范式。如果无论怎样代入,总存在可以继续代入的子项,那么它就没有范式。

比如M=λx.(xx) λx.(xx),则无论经过多少次代入,归约的路径都是其本身,所有M就是n.n.f.。

 

Church-Rosser定理

 

如果λ-项M有一个范式,那么这个范式是唯一的,并且β-归约的路径必将终止,而且终止到这个范式。

Church-Rosser定理:如果λ├M=N,则对某一个Z,λ├MàZ并且λ├NàZ。

与之等价的定理如下,

Diamond Property定理:如果MàN1,MàN2,则存在某一Z,使得N1àZ,N2àZ。

 

 

科里化(Currying)

 

科里化的概念最早由俄国数学家Moses Schönfinkel引入,而后由著名的数理逻辑学家哈斯格尔·科里(Haskell Curry)将其丰富和发展,Currying由此得名。它表示一种将一个带有N元组参数的函数转换成N一个一元函数链的方法。由于λ演算用到的都是一元函数,在有多个变量的情况下就要使用科里化了。

科里化的过程非常直观,假设一个函数有N个参数,则先固定一个参数,返回一个具有N-1个参数的函数,再对这个函数进行科里化。

假设一个函数f(x,y,z)=x/y+z,要求f(4,2,1)的值

首先用4替换f(x,y,z)中的x,得到函数g(y,z)=4/y+z

然后用2替换g(y,z)中的y,得到函数h(z)=4/2+z=z+2

最后用1替换h(z)中的z,得到h(1)=3

在科里化的步骤中,每一步代入一个参数,最终得到一个嵌套的一元函数链。通过科里化,可以对任何一个多元函数进行化简,使之能够进行λ演算。

现在我们可以处理一元函数和多元函数的计算,对于一些非常微小的程序已经可以用形式化的方法展示。

 

不动点理论

不动点理论的诞生是为了处理递归的问题。递归是使用函数自身的函数定义;在表面上,λ演算不允许这样做。

考虑阶乘函数f(n)递归的定义为

f(n)=(n==0)?1:n*f(n-1)

在λ演算中,不能定义一个包含了自身的函数。为了避免这样做,必须开始一个新的入口函数g,它接收一个函数f和一个整数n作为它的参数

g(f,n)=(n==0)?1:n*f(n-1)

那么对g进行科里化,之后得出的λ表达式如下:

g=λf.( λn.(n==0)?1:n*(f(n-1)))

但是g本身又不是递归的,为了使用g来建立递归函数,作为参数传递给g的函数f必须有特殊的性质,也就是说,作为参数传递的f函数必须展开为调用带有一个参数的函数g,并且这个参数必须再是f函数。

换句话说,f必须展开为g(f),这个到g的调用将接着展开为上面的阶乘函数并计算下至另一层递归。然后f再次出现,并再次被展开为g(f),才能继续递归。

这里的f=g(f),叫做g的不动点,它可以在λ演算中使用不动点算子来实现,它叫做Y组合子:

Y=λg.( λx.g(x x)) (λx.g(x x))

在λ演算中,Y g是g的不动点,因为它展开为g(Y g)。现在我们调用g(Y g,5),计算5的阶乘。

它展开为:

λf.(λn.(n==0)?1:n*f(n-1) (5)) (Y g)

然后f(4)展开之后是:

g(Y g,4)

之后就是一直递归到g(Y g,0).

递归的求值算法的结构,都可以被表达为某个适当函数的不动点,因此所有使用递归定义的函数都可以表达为λ表达式。

现在多元函数、递归函数都可以用λ表达式来表示,它在计算性上已经是无缺陷的了。

 

荣光的焕发?函数式编程!

 

函数式编程并不是一个新兴的词语。约翰巴克斯在他1977年图灵奖的演讲“编程语言可以从冯·诺依曼风格中被解放吗?一种函数式的风格以及其程序中的代数学”中展示了FP语言。巴克斯的论文使得函数式编程的研究受到欢迎。在20世纪80年代,Per Martin-Löf开发了直观类型理论,将函数式编程和表现形式为依赖类型的任意复杂的数学命题的构造性证明联系起来。这引导了新的证明交互式定理的强大方法并且影响了许多后来的函数式编程语言的开发。

函数式编程中拥有大量特定的概念和编程范式,并且和指令式编程(包括面向对象编程)是无关的。然而,编程语言经常会是多种编程范式的杂交种,因此这些概念可能会被用到。

在函数式编程中,首要的概念叫做初类函数(first-class function)。初类函数和高阶函数(Higher-Order function)有着非常紧密的联系,它们都可以允许函数作为其它函数的参数或者返回值。“高阶”描述了一个数学概念,比如微分算子d/dx,接受一个函数作为参数,返回一个函数作为返回值。高阶函数也实现了偏函数应用和科里化,使一个多元函数变成一个嵌套的一元函数链。

函数式编程将计算看做是数学函数的值,并且避免状态和可变数据。函数式编程强调那些只由输入数据决定而不是程序状态决定计算结果的函数,换言之叫做纯函数。在函数式的代码当中,一个函数的输出值只取决于该函数的输入参数,因此调用同一个函数f,如果参数x相同,那么将会输出同样的结果f(x)两次。也就是说,函数的返回值不依赖于程序现在的状态比如static类型的变量的值,这样做使得理解和预测一个程序的行为变得更加容易。

纯函数式的函数或者表达式没有副作用(内存或者I/O)。这意味着纯函数有许多有用的属性,其中许多可以用来优化代码。如果两个纯函数式的表达式之间没有数据的依赖性,那么他们的顺序是可以被调换的,或者他们可以悲哀并行执行而且不能互相干扰,换句话说纯函数式的表达式或函数是线程安全的。并且这样的特性给予编译器绝对的自由去重新排序或者组合程序中表达式和函数的计算,可以规避一些由编译器带来的不确定性因素比如静态初始化的相依性。

由于函数式编程有种种优秀的特性,使得一直以来被广泛在学术领域上强调而不是商业软件开发的函数式编程在工程中重新焕发荣光。

但是没有一种编程范式是完美的。面向对象编程的问题在于它对对象的定义,它试图将所有事情就纳入到这个概念里,当这个思想极端化之后,就会得出一个一切皆为对象的思想。但是这种思想并不一定是正确的,因为有些东西不是对象,函数就不是对象。而妄图将世界抽象成一个一个的对象的结果是,函数被“绑架”,被监禁在“对象”里,反而增加了设计的难度。

而相似地,函数式编程走向极端,成为了纯函数式编程,但是纯函数式编程面临的问题是——有些东西不是纯的,副作用是真实存在的。纯函数式编程试图通过函数,在函数中传入传出整个宇宙来实现整个宇宙。但是现实生活中副作用确实存在。回顾一下函数式编程背后的可计算性理论,λ演算和图灵机的区别恰恰在于,λ演算更偏向于数学意义而图灵机具有更深层的物理实际意义是真正的抽象计算机。数学上的理想主义是纯函数编程语言的背后推力。毫无疑问,数学函数是简单漂亮的,但不幸的是,生活不是数学,就算是数学建模也有不能模拟的因素存在。

因此纯粹的面向对象和纯粹的函数式编程在一定程度上都是具有问题的。无论任何事情,走向极端都是有害的。正因为如此,java、C++、C#等等现在流行的编程语言都加入了函数式编程的元素。一个好的编程语言应该吸收多种编程范式而不能极端地拘泥于一种,因为世界不会适应程序员的模型,反而应该是程序员的模型去适应这个世界。

0 0
原创粉丝点击