λ演算

λ 演算

时下函数式程序设计风格甚是流行,新出的语言,比如 F#、golang 天然支持;旧的语言 c++、c# 等则在不断添加对它们的支持。函数式程序设计的主要特点就是:值(value)是不变量;函数(function)可以像值一样放到数据结构中,也可以像普通的值一样作为函数的参数和返回值。不过函数式风格和其他的命令式风格最大的不同并不在于语法上,而是在于思维习惯上。函数式强调的是函数组合,而命令式强调的是函数的编写;前者的编程更像是 what to do ,而后者的其实是 how to do (肿么做)。扯远了,O__O"…

λ 演算是函数式程序设计语言的基础数学理论;看一看神马是 λ 演算可能对 coding 没有太大帮助,但是可以涨涨见识,到时候吹牛也有点谱,O__O"…

λ表达式

在 λ 演算中,一切都是 λ 表达式,普通名字(标识符),比如 a 是 λ 表达式;函数 f(x)=x+2 是 λ 表达式;函数调用 f(4) 也是 λ 表达式(不过在 λ演算中,me 们要问问自己,2、4 是神马东西? + 又是神马东西?)[函数调用是个编程词汇,数学中可能更多滴说作用/应用,总之下面说函数调用、参数作用/应用于函数意思相同 ]。有这三种,也仅有这三种 λ 表达式:

  • <expr> ::= <identifier>
  • <expr> ::= λ <identifier> . <expr>
  • <expr> ::= <expr> <expr>

::= 是定义的意思,<> 括起来的是一些术语,比如 <expr> 指的就是 λ 表达式。三种 λ 表达式的第一种就是说的 a、b、c 、x、y、z 这样的表达式;第二种是函数的定义式,不过和 f(x) = x+2 不同的是写成 λx.x+2;第三种是函数调用式,f(4) 写成 f 4。不过在 λ 演算中很少看到 2、42 这样的值,因为 λ 演算强调函数演算,me 们看到的基本都是如同 λx.x y z 这样的表达式;当然在后面会发现,2、42 这样的自然数其实可以用函数表示。(神马!值怎么会是函数?!)

乍一看,貌似 λ 表达式并没有什么神奇的地方,标识符(最简单的代数式)、函数定义和函数调用,这些数学和编程中已经使用烂了的东西,能有神马新颖呢?难道说就是因为形式变了,将 f(x) = x+1 写成 λx.x+1 ? 将函数调用 f(4) 写成 f 4 ?实际上, λ 表达式中的函数定义,参数可以是任意 λ表达式,映射的结果也可以是任意 λ 表达式。即使这样,恐怕 me 们也不很难认识到这样简单的表达式背后能蕴藏着什么。

使用 λ 表达式可以定义自然数,定义自然数的加法、乘法,定义条件判断,定义递归,最后有人说,一切可以计算的函数都可以用 λ 表达式表示;而只有可以用 λ 表达式表示的函数才是可计算函数,me 们诧异了! λ 表达式的计算能力,是人的计算能力?!关于 λ 表达式目前再补充几句:

  1. 使用括号的表达式 (E) 和表达式 E 有同等的意义,加括号可以避免歧义;
  2. 在没有加括号的时候,函数应用的参数满足左结合,比如 E a b c 的意义是:((E a) b) c ;
  3. 在没有加括号的时候,函数定义的函数体部分(映射部分,也就是 . 号后面的部分)满足右结合,比如 λx. λy.x y 的意义是:λx. (λy.(x y));
  4. λx. λy.λz.x y z 简写成:λxyz.x y z;实际上这就是多元函数丫,有木有;

绑定变元和自由变元

λ 表达式中的变元分成两部分:绑定变元和自由变元,比如 λx.x y,x 是绑定变元,y 是自由变元;

  1. 绑定变元就是 . 号前面的变元,在函数应用的时候会被替换掉,比如 (λx.x y) a == a y ;
  2. 自由变元就是没有出现在 . 号之前的标识符,在函数应用的时候,自由变元不被替换;
  3. 绑定变元是占位符,使用什么标识符无关紧要,就像 f(x) = x 和 f(y) = y 是同一个函数一样, λx.x 和 λy.y 是一样的;
  4. 在 E U 表达中,自由变元是 E 中自由变元和 U 中自由变元的并集,比如 (λx.x y) (λz.z x) 中的自由变元有前面中的 y 和后面中的 x;

化简

一个复杂的 λ 表达式往往可以化简,化简的一个主要方法就是将函数参数应用到函数中去,比如 f(x)=x+2 时 f(a+1) 实际就是 a+3。 λ 表达式的化简要保证前后的式子等价,具体的化简(变换)方法有三种:

  1. α 转换:α 转换就是将函数定义中的绑定变元用其他字母替换掉,比如 λx.x 转换成 λy.y;
  2. β 消解:也就是在函数应用中将参数带入函数体中,比如 (λx.x) a 实际就是 a ;
  3. η 转换:如果两个函数对于所有的输入都有相同的输出,那么这两个函数就应该是等价的,可以 η 转换;

上面的三种转换对应三种等价式,不过通常的化简可能要用到上面的一种、两种或是三种。

替换

λ 表达式中的前两种,也就是 x 和 λ x.x 这两种没有太多神秘的地方,然而对于第三种也就是参数应用于函数的时候,化简起来要注意一些东西。函数实际上是一种映射关系,比如 f(x) = x+2 就是将所有的 x 映射成 x+2,当然也可以认为是一种替换,将所有 = 右侧的自由变元替换成实际的参数,比如 f(x) = sin(x) + cos(|x|) 对于 f(a+1) 的结果就是 sin(a+1) + cos(|a+1|)。λ 表达式的替换也是如此,不过要注意的是,只能替换绑定变元而不能是自由变元。

现在引入一种记号,虽然 me 赶脚这点有点多余,比如 (λx.E) P ,me 们需要使用 P 替换所有 E 中 x 作为绑定的地方 ,这个替换记作:[P/x]E 或是 [x:P]E 或是 E[x:P],怎么记相对来说无关紧要,me 这里都使用前者。me 们现在也希望用一些字母来指代具体的 λ 表达式,这样有时候写起来方便点,比如恒等函数 f(x) = x 也就是 λx.x 记作 I ,那么以后出现 I 就是指代的 λx.x 。

现在看下 I I 是神马表达式。I I = ( λx.x )I = [I/x]x = I,也就是 I I 和 I 是等价式。再看一个 ( λx.( λy.xy))y 化简后是神马?y 要作用于前面的函数,me 们用它去替换函数的函数体部分中的被绑定的 x,也就是 [y/x](λy.xy),如果简单滴写成: λy.yy 就 错了。函数中的 λy.xy 函数定义部分中的绑定变元 y,me 们是正巧用了 y 而已,换成 t 不影响,但是如果在 me 们替换的时候不提前区分这代表不同意义的 y,那么结果就混淆了绑定变元 y 和应用参数 y;实际正确的结果应该是:λt.yt,O__O"…

关于 λ 演算的基础理论貌似只有上面那些,不过对于该理论的延伸,或是说 λ 演算能有多大的 power,下面慢慢叙述。

自然数

自然数就是 0 1 2 3 4 5 6 7 8 ...,从 0 开始,每个 n 后面都有一个 n+1,而不会出现循环,比如 0 1 2 3 4 0 。自然数本来就是一个抽象的概念,me 们可以用 0 1 10 11 100 101 110 111 1000 ... 指代自然数,懂二进制的一眼可以看出来这是十进制的二进制表示,不过 me 想说的是这里只是选取了另外一组比较有规律的表示而已。me 们也可以用 ε 1 11 111 1111 11111 111111 1111111 11111111 ... 来指代自然数,实际上 me 们可以使用任意一个可数无限集合的元素的一种枚举方法作为自然数的一种表示,比如 0 2 4 6 8 10 12 14 16...;所谓可数,也叫可枚举,就是可以如同自然数一般一一数出来所有元素

自然数定义

在 λ 演算中,我们希望提取一个可数函数集合来作为自然数的表示,这种表示越简单越好,但是简单到么法在其基础上建立模型就不大好了(Keep one thing simple and simple, but no simpler)。历史上有人发现过一种定义方式,me 这里直接拿来用了:

0 := λf.λx.x
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)))

这里对二元函数简单说明一下,虽然上面已经说明过了,O__O"… f(x,y) = x+y 是二元函数,如果 me 们传递一个参数,比如 f(42, y) = 42+y,这便是一个一元函数;在 λ 表达式中表示这个二元函数是这样的 λx. (λy.x+y),如果约定函数体右结合那么便可以去掉 (),于是便成了λx. λy.x+y ;但是为了看起来方便点,me 们可以简写成:λxy.x+y。这就是二元函数的书写。上面的自然数都定义成带有两个参数的函数,当然也可以理解成带有一个函数参数,返回值是另外一个函数。

SUCC 后继函数

可以看出来,从 0 开始, me 们可以不断滴定义自然数,这里 me 们找一个后继函数 SUCC,也就是根据 n 生成 n+1。SUCC 的作用应该是这样的:SUCC 0 = 1, SUCC 1 = 2, SUCC 2 = 3, SUCC 3 = 4, ...., SUCC n = n+1...

SUCC := λn.λf.λx.f (n f x)

验证 SUCC 2 = 3:SUCC 2 = [2/n] (λf.λx.f (n f x)) = λf.λx.f (2 f x) = λf.λx.f (f (f x)) = 3。这里关紧的一步是:2 f x = f (f x) 。

PLUS 加法函数

虽然通常 me 们 2+3 这样写,实际上更好的理解是 + 2 3,也就是加法的操作是一个函数,带有两个参数,分别是 m 和 n (当然 m 和 n 这里也是函数)。有了 SUCC 函数,me 们可以认为实际上PLUS m n 是在 m 的基础上 SUCC 了 n 次

me 们再回看一下自然数的的定义,比如 3,带有两个参数 f 和 x,f 出现的次数是 3,而最初的 f 应用的参数是 x,是不是认为 3 这个函数的两个参数的意义就是:对 x 使用 f 作用 3 次? 现在 me 们看一下 2 SUCC 3 会是神马结果:2 SUCC 3 = SUCC (SUCC 3) = SUCC 4 = 5,这个时候 me 们会惊奇上面自然数定义的甚是巧妙,因为当 me 们使用 SUCC 去替换 n 中的 f 、使用 m 替换 x 的时候,就是对 m SUCC 了 n 次!这岂不就是 m+n ?O__O"… 所以,PLUS 可以定义为:

PLUS := λm.λn.n SUCC m

MUL 乘法函数

在 SUCC 的基础上建立了 PLUS,me 们可能会想在 PLUS 的基础上建立 MULT。× 2 3 实际可以认为是在 0 的基础上 PLUS 2 了 3 次,也就是 MULT 2 3 = (PLUS 2) (PLUS 2 (PLUS 2 0)) = 3 (PLUS 2) 0,所以 PLUS 可以定义为:

MULT := λm.λn.n (PLUS m) 0

POW 指数函数

2 ³ = POW 2 3,在 1 的基础上乘以 2 三次,在有了上面的经验之后,me 们可以比较容易滴得到 POW = λm.λn.n (MULT m) 1。不过 POW 还有一个更简单的表示,如下,这里不再多提。

POW = λm.λn.n (MULT m) 1
POW := λb.λe.e b

其他

还有前驱函数 PRED,还有减法函数 SUB,不过 me 现在还没有搞懂,不过现在也不打算继续了,适合而止,下面粘贴了 wiki 上的结论,仅供参考:

PRED := λn.λf.λx.n (λg.λh.h (g f)) (λu.x) (λu.u)
SUB := λm.λn.n PRED m

非数值计算

前面一贴让 me 体验了一下 λ 表达式可以表示自然数和运算,而且多次出现了n 次这个字眼,编程中可能会说是循环,不过上面可完全没有出现类似于 while、for 和 until 的结构,只是函数组合就出现了循环的结果,这可能就是函数式编程的特色吧。结构化编程的另一个重要控制结构 —— 分支,me 们可能要纳闷儿是不是也可以用 λ 表达式表示,下面就是介绍这些内容,也就是使用 λ 表达式表示逻辑、以及递归

布尔逻辑

如同可以使用不同的函数来表示 0、1、2 一样,me 们也可以使用任意两个不同的函数来表示 TRUE 和 FALSE,不过还是和自然数表示类似,选取合适的表示,有助于建立其他运算。下面就是一种表示:

TRUE := λx.λy.x
FALSE := λx.λy.y

眼光好的,可以发现 FALSE 的定义和 0 的定义是一样的,O__O"…不过这个无伤大雅,也不能说明它们之间有神马必然联系。布尔值的三种基本运算是:AND、OR 和 NOT,具体神马意义就不多说了,下面给出 λ表达式表示:

AND := λp.λq.p q p
OR := λp.λq.p p q
NOT := λp.λa.λb.p b a

if P then Q else R 这样的分支的表示:

if P then Q else R := λP.λQ.λR. P Q R

上面这些都可以自己去验证,至于为什么“猜”出来可以这样表示,me 暂时也不深究了,O__O"…

二元组

二元组是一个重要的概念,pair(a,b) 中的 a、b 是有顺序之分的,pair(2,3) 和 pair(3,2) 不是一个二元组。下面给出定义:

PAIR := λx.λy.λf.f x y
FIRST := λp.p TRUE
SECOND := λp.p FALSE
NIL := λx.TRUE
NULL := λp.p (λx.λy.FALSE)

ISZERO := λn.n (λx.FALSE) TRUE
LEQ := λm.λn.ISZERO (SUB m n)

递归

在数学和编程中都经常出现递归这个概念,数学中递归常用来定义概念,比如阶乘 n! ,比如斐波那契数,而编程的递归则是在函数直接或间接地调用自己;编程中的递归基本是照着递归的定义写的,所以这里不关心到底是数学中的还是编程中的。

f(n) = n! 的递归定义:if n = 0 then f(0) = 1 else f(n) = n×f(n-1)

if...then..else 结构和乘法×在 λ 表达式前面已经定义了(相等的比较可以用小于LEQ 和 AND 联合实现),现在 me 们关心的是 λ表达式中能自己调用自己吗?λ 表达式中的函数都没有名字,这样的话,它自己怎么去引用自己呢?me 们可以简单滴借助一个参数实现:

G := λr. λn.(1, if n = 0; else n × (r r (n−1)))
F := G G = (λx.x x) G

上面的 G 除了参数 n 之外又引入了一个参数 r,这样函数体中就可以递归调用自己勒;然而问题 n! 的计算不是 G n,而是 G G n。对于 F 4 = G G 4 = 1, if 4 = 0; else 4×(G G 3) = 4×(G G 3 ) = 4×3×(G G 2) = ... = 4 × 3 × 2 × 1 × 1 = 24。这样 F := G G 实际就满足了 me 们的要求,以后 me 们对于写的递归函数都需要 G G 类似方式的调用。

me 们可能希望有这样的一种形式:Y G 就可以递归调用,而 Y 是独立于 G 的一个通用的函数,而不像上面的 G G。me 们期望的:Y G x = G (Y G) x,这样 x 可以直接应用于 G,而 Y G 也递归滴传到了 G 中。下面就有一个这样的 Y:

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

这一部分相对于前面的算术计算来说,me 了解的并不太深入,只是搜到了一些 AND、OR 等等的函数表达式而已,至于是如何来的,如何想到的,有木有启发性的,暂时管不了那么多了,这部分只算是开个眼界,知道原来所谓的逻辑和诸如递归这样的东西,都是可以用 λ表达式描述的。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值