形式语义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^*\) 定义为满足如下条件的最小集合

  1. \(x\in\mathcal V\),那么 \(x\in\Lambda\)
  2. \(M,N\in\Lambda\),那么 \((MN)\in\Lambda\),此时称 \(N\)parameter
  3. \(x\in\mathcal V\)\(M\in\Lambda\),那么 \((\lambda x.M)\in\Lambda\),此时称 \(M\) 为这个term的body

上面三种形式分别叫做variableapplication\(\lambda\) abstraction(function)

为了简略(这也太简略了),有一些约定俗成的优先级和结合律:

  1. 最外层括号可以省略不写,例如 \((\lambda x.x^2)=\lambda x.x^2\)
  2. Application形式是左结合的,即 \(f\;x\;y\;z=((f\;x)\;y)\;z\)
  3. 一个\(\lambda\)的约束范围(scope)向右延伸,body的范围尽可能长。例如 \(\lambda x. x+y=\lambda x.(x+y)\neq(\lambda x. x)+y\)
  4. 多个\(\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)\) 这个记号来表示

容易有如下规则:

  1. \(FV(x)=\left\{\;x\;\right\},\; x\in\mathcal V\)
  2. \(FV(\lambda x.M)=FV(M)-\left\{\;x\;\right\}\)
  3. \(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

前面说到对函数的形参可以任意换名但是有条件。考虑以下三种情况:

  1. 作替换\([x/z]\),这时候第5行变为return x + y + x;,且这里的两个x都被绑定到形参int x上(形参也被替换了),含义和原文显然是不同的
  2. 作替换\([u/z]\),这时候u从未出现过(称为fresh variable),这个替换就没有任何问题
  3. 作替换\([z/x]\),这时候x从free variable变成了bound variable,含义也发生了变化

基于如上考虑,我们给出substitution的递归定义:

  1. \(x[N/x]\overset{\text{def}} =N\)
  2. \(y[N/x]\overset{\text{def}}=y\)
  3. \((M N)[P/x]=(M[P/x])(N[P/x])\)
  4. \(\lambda x.M[N/x]=\lambda x.M\)
  5. \(y\not\in FV(N)\),那么 \((\lambda y.M)[N/x]=\lambda y.(M[N/x])\)
  6. \(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。递归定义如下:

  1. \((\lambda x. M)N\rightarrow M[N/x]\),这就是基本的\(\beta\)-reduction
  2. 如果 \(M\rightarrow M'\),那么 \(\lambda x.M\rightarrow\lambda x.M'\)
  3. 如果 \(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”,只是说

  1. 如果从同一个term开始推导,那么两个推导序列中将会存在一个公共项(不一定是normal form)
  2. 如果一个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的区别在于

  1. Evaluation只要求最后是一个特殊的形式(canonical form)
  2. 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的求值策略,规则如下

  1. \(\lambda x. M\Rightarrow \lambda x.M\),意思是在此处停止
  2. 如果\(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,再进行形参代换

  1. \(\lambda x. M\Rightarrow \lambda x. M\)
  2. \(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)))))\)

posted @ 2021-09-16 11:36  jjppp  阅读(207)  评论(0编辑  收藏  举报