形式语义03 Lambda
\(\lambda\)-calculus
Background
首先这是一种编程语言,在1930s被Alonzo Church和Stephen Cole Kleene发明(两位都是听说过的明星人物)
还是一种计算模型,在1937年被Alan Turing证明其和图灵机的表达能力等价(这位更是重量级)
\(\lambda\) 演算是函数式编程的基础,同时它简单的特点也使得很适合用于研究PL的各个领域(回忆IDFS中的\(\lambda\) 表达式作为transfer function)
PL的东西有点乱,大家的说法和记号也都不一样,这里选择了可能算是比较好理解又简洁的一种
Syntax
定义 \(\mathcal V\) 是无穷变量集合,\(\Sigma=\mathcal V\cup\left\{(,),\lambda,.\right\}\) 为字符集,那么定义一个term为\(\Sigma\)上满足的某些约束的有穷串
\(\lambda\) calculus term组成的集合 \(\Lambda\subseteq \Sigma^*\) 定义为满足如下条件的最小集合:
- 若 \(x\in\mathcal V\),那么 \(x\in\Lambda\)
- 若 \(M,N\in\Lambda\),那么 \((MN)\in\Lambda\),此时称 \(N\) 是parameter
- 若 \(x\in\mathcal V\) 且 \(M\in\Lambda\),那么 \((\lambda x.M)\in\Lambda\),此时称 \(M\) 为这个term的body
上面三种形式分别叫做variable、application、\(\lambda\) abstraction(function)
为了简略(这也太简略了),有一些约定俗成的优先级和结合律:
- 最外层括号可以省略不写,例如 \((\lambda x.x^2)=\lambda x.x^2\)
- Application形式是左结合的,即 \(f\;x\;y\;z=((f\;x)\;y)\;z\)
- 一个\(\lambda\)的约束范围(scope)向右延伸,body的范围尽可能长。例如 \(\lambda x. x+y=\lambda x.(x+y)\neq(\lambda x. x)+y\)
- 多个\(\lambda\) 可以写在一起。例如 \(\lambda x.\lambda y.\lambda z.=\lambda xyz.\)
Semantics
Bound & Free Variables
对于一个形如 \(\lambda x.N\) 的term,我们就说 \(N\) 中出现的 \(x\) 都被绑定(binding)到了 \(\lambda x.\) 上,并把 \(\lambda x.\) 叫做binder
注意绑定是可以嵌套的,例如 \(\lambda x. (\lambda f.\lambda x.f\; x)+x\),最里面的 \(x\) 被绑定到了第二个 \(\lambda x.\),而最后一个 \(x\) 则被绑定到了第一个 \(\lambda x.\) 这个规则和C的作用域是很相似的
如果一个变量没有被绑定,就被称为自由变量(free variables)
一个bound variable是可以被换名字而不改变term的含义的,类似于C function中形参的名字其实是任意的(但仍然有限制,后面会说)。而free variable则不能随意换名字。因此我们主要关注一个term \(M\) 中具体的free variables有哪些,用 \(FV(M)\) 这个记号来表示
容易有如下规则:
- \(FV(x)=\left\{\;x\;\right\},\; x\in\mathcal V\)
- \(FV(\lambda x.M)=FV(M)-\left\{\;x\;\right\}\)
- \(FV(M N)=FV(M)\cup FV(N)\)
也就是结构归纳
Substitution
这里的substitution和直接替换是有区别的,注意我们substitute的必须是free variable
我们用 \(M[x/y]\) 或者 \(M\left\{x/y\right\}\)、\(M[y:=x]\) 来表示用 \(x\) 替换掉term \(M\) 中的 \(y\) 的结果,并把一次substitution记作 \(M\rightarrow M[x/y]\)
考虑如下C代码
int x=1;
int y=2;
int foo(int z) {
return x + y + z;
}
这里第5行中,x,y
是free variable,z
被绑定到形参int z
前面说到对函数的形参可以任意换名但是有条件。考虑以下三种情况:
- 作替换\([x/z]\),这时候第5行变为
return x + y + x;
,且这里的两个x
都被绑定到形参int x
上(形参也被替换了),含义和原文显然是不同的 - 作替换\([u/z]\),这时候
u
从未出现过(称为fresh variable),这个替换就没有任何问题 - 作替换\([z/x]\),这时候
x
从free variable变成了bound variable,含义也发生了变化
基于如上考虑,我们给出substitution的递归定义:
- \(x[N/x]\overset{\text{def}} =N\)
- \(y[N/x]\overset{\text{def}}=y\)
- \((M N)[P/x]=(M[P/x])(N[P/x])\)
- \(\lambda x.M[N/x]=\lambda x.M\)
- 若 \(y\not\in FV(N)\),那么 \((\lambda y.M)[N/x]=\lambda y.(M[N/x])\)
- 若 \(y\in FV(N)\),那么 \((\lambda y.M)[N/x]=\lambda z.(M[z/y][N/x])\)
1234都好理解,主要说说56
5的意思是如果 \([N/x]\) 不会引入新的 与当前形参重名的free variable,那么就直接换
6的意思是如果非要换,那么就得先把 \(M\) 中被绑定到当前 \(\lambda y.\) 上的 \(y\) 都换成一个fresh variable \(z\),此时就规约成5的情况了。最开始我以为也可以先换掉 \(N\) 中的 \(y\),但是注意free variable的含义,如果换掉了的话语义就变了。
\(\alpha\)-conversion
也叫\(\alpha\)-renaming,意思是我们把形如 \(\lambda x. M\) 这样的term的body中与binder同名的变量连同binder一起改名字(改成一个从未出现过的变量)得到的term和原来等价。这样的等价关系叫做\(\alpha\)-equivalence、\(\alpha\)-congruence。
\(\beta\)-reduction
讲的是如何reduce一个term。递归定义如下:
- \((\lambda x. M)N\rightarrow M[N/x]\),这就是基本的\(\beta\)-reduction
- 如果 \(M\rightarrow M'\),那么 \(\lambda x.M\rightarrow\lambda x.M'\)
- 如果 \(M\rightarrow M'\),那么 \(M N\rightarrow M' N\),\(N M\rightarrow N M'\)
\(\beta\)-reduction是满足上述条件的最小二元关系,显然是自反、传递的
再定义 \(\overset*\rightarrow\) 是 \(\beta\)-reduction的自反传递闭包,这个在后面会用来刻画confluence性质
\(\beta\)-redex 和 \(\beta\)-normal form
引入这俩的意图是为了判断reduction何时停止
\(\beta\)-redex=\(\beta\)-reduction expression,意思是形如 \((\lambda x.M)N\) 这样的term。这样的形式仍然可以根据\(\beta\)-reduction rule来进一步化简
\(\beta\)-normal form指的是不含\(\beta\)-redex的项,注意到所有的reduction都是基于\(\beta\)-reduction定义的,不含\(\beta\)-redex意味着不能再reduce。如果我们把reduction看成映射,那么就可以认为是到达了一个reduction操作下的不动点
\(\eta\)-reduction
这个主要是利用了函数的外延性等价。意思是说,如果两个函数对于相同的输入有相同的输出,那么它们就可以互相替换
一个例子也是从sicp中来的。假如我们要构造有理分数这一数据类型,并打算用pair来构造,那么可以写成如下形式:
(define (make-rat a b) (cons a b))
也可以这么写:
(define make-rat cons)
具体到\(\lambda\)-calculus就是:
若 \(f=g\),那么 \(\lambda x. f\; x=\lambda x.g\; x\)
Confluence
Church-Rosser Confluence Theorem:
对于任意的term \(M\),若存在两个不同的reduction序列使得 \(M\overset*\rightarrow A\),\(M\overset*\rightarrow B\),那么就必然存在一个term \(N\) 使得 \(A\overset*\rightarrow N\) 且 \(B\overset*\rightarrow N\)。注意\(A\)、\(B\)、\(N\)都有可能相等
推论:在\(\alpha\)-equivalence下,每个term如果存在\(\beta\)-normal form,那么这个形式是唯一的
反证即可,如果存在两个那么就违反了上述定理。
需要注意的是,这里并没有说“任意推导序列都能得到normal form”,只是说
- 如果从同一个term开始推导,那么两个推导序列中将会存在一个公共项(不一定是normal form)
- 如果一个term开始推导能得到一个normal form,那么这个形式在\(\alpha\)-equivalence意义下唯一
Reduction Strategies
回忆scheme求值的正则序和应用序(即先代换形参还是先对实参求值)策略,正好对应了\(\lambda\)-calculus的不同reduction"路径",也就是策略。
实际上就是取 \(\rightarrow_\beta\) 这个二元关系的一个子集,在牺牲一些推导能力的前提下使每步推导确定下来
之所以会出现策略的不同,是因为一个term可能存在多个\(\beta\)-redex,每一步的选取就造成了化简序列的差异。而虽然我们知道normal form唯一,但并不是所有的推导序列都能终止,也并不是所有的term都有normal form。
一个比较好玩的例子就是 \((\lambda x.x\; x)(\lambda x.x\; x)\),这就是没有normal form的term
利用上面的例子可以构造出如下式子 \((\lambda u.\lambda v.v)((\lambda x.x\; x)(\lambda x.x\; x))\),它就存在一个无法终止的推导序列
Normal-order reduction
每次选择最左、最外的redex
有定理:Normal-order reduction一定能找到normal form(如果存在的话)
Applicative-order reduction
每次选择最左、最内的redex
这两种规约的效率谁更优是不一定的
Evaluation Strategy
好像搜了很多地方都没有找到类似的定义,可能这是村规
Evaluation和Reduction的区别在于
- Evaluation只要求最后是一个特殊的形式(canonical form)
- Evaluation会尽可能避免对function body化简
Canonical Form
意思是形如 \(\lambda x. M\) 这样的term
一个Closed Normal Form一定是Canonical Form(所有变量都bounded,且不能再规约,意味着最外面是\(\lambda x. M\) 的形式),反之则不然(很显然一个CF仍然可能被规约简化)
Normal Order Evaluation Rules
注意到,如果normal order reduction停止了,那么在规约过程中一定存在一个canonical form。因此提出normal order evaluation的求值策略,规则如下
- \(\lambda x. M\Rightarrow \lambda x.M\),意思是在此处停止
- 如果\(M\Rightarrow\lambda x. M'\),且$M'[N/x]\Rightarrow \(P,那么\)M; N\Rightarrow P$,意思是先把function body规约成canonical form(也就是一个标准的function形式),再带入parameter,最后整体化简
Eager Evaluation Rules
这个考虑的则是applicative order reduction的evaluation,即先把parameter化成CF,再进行形参代换
- \(\lambda x. M\Rightarrow \lambda x. M\)
- 若\(M\Rightarrow \lambda x.M'\),\(N\Rightarrow N'\),\(M'[N'/x]\Rightarrow P\),那么 \(M\; N\Rightarrow P\),注意观察与上面的不同
Fun Func
给几个好玩的例子吧,可以在lambda calculus interpreter里面玩一玩
Bool
\(True=\lambda x.\lambda y. x\)
$False=\lambda x.\lambda y. y $
编码其实是任意的,类比函数等价定义的外延性(在相同输入下有相同输出),数据等价定义为它们在相同操作下有相同的行为
negate就比较取巧了
\(negate = \forall b. b\;False\;True\)
然后可以写一个mux,可以方便后面的二元函数
即\(if(A)then(B)else(C)=(A\;B)\;C\)
于是就可以很容易地写出and和or和xor,实际上只需要写出三个中的一个就完备了
\(and=\lambda x.\lambda y. if(x)then(y)else(False)\)
\(or=\lambda x.\lambda y.if(x)then(True)else(y)\)
\(xor=\lambda x.\lambda y.if(x)then(negate\; y)else(y)\)
Natural
如何判断一个自然数是\(0\)
\(isZero=\lambda n.(n\;(\lambda x.False)\;True)\)
注意到当\(n=0\)时得到的是单位函数,否则得到常函数\(False\)
其他部分都快写烂了,跳
Recursion
这个比较好玩,之前算是没怎么搞懂
还是那个玩烂了的例子,我们要算阶乘
很容易写出 \(fact=\lambda n.if (isZero\;n)then(1)else(mult\;n\;(fact\;(pred\; n)))\)
问题在于等式两侧都出现了\(fact\),在具体语言中就表现为我们必须给递归函数一个名字才能调用递归
接下来就是很神奇的操作了
考虑这个函数 \(Func=\lambda f.\lambda n.if (isZero\;n)then(1)else(f\;n\;(f\;(pred\; n)))\),它接受一个函数 \(f\),返回一个\(f\)的递归调用。那么上面的定义就可以解释为\(fact\)是函数\(Func\)的不动点,即\(Func\;fact=fact\)
也就是说,虽然我们不能给\(fact\)命名,但是我们可以通过一个不含\(fact\)的式子把它算出来
算不动点也是很神奇的操作
回忆之前的神奇表达式 \(\Omega=(\lambda x.x\; x)\,(\lambda x.x\; x)\),可以构造(怎么想到的?!)一个新的函数
\(Y=\lambda F.(\lambda x.F(x\;x))\,(\lambda x.F(x\;x))\)
观察 \(YF=(\lambda x.F(x\; x))\,(\lambda x.F(x\;x))=F((\lambda x.F(x\;x))\,(\lambda x. F(x\;x)))=F(YF)\)
大概的idea就是我们希望每次apply完之后,前面多出一个\(F\)而后面保持不变,这样就可以得到不动点
那么阶乘就可以写成
\(\lambda n.if(isZero\; n)then(1)else(mult\;n\;((Y\;(\lambda f.\lambda n.if (isZero\;n)then(1)else(f\;n\;(f\;(pred\; n))))\; (pred\; n)))))\)
本文来自博客园,作者:jjppp。本博客所有文章除特别声明外,均采用CC BY-SA 4.0 协议