5-The Untyped Lambda Calculus

引入

lambda 演算中,所有事物都是函数

lambda 演算式中有三种项:

  • 变量 x,形如 x

  • 函数抽象 abstraction,形如 λx . t1

  • 将项 t1 作用于 t2,形如 t1 t2

有如下的简单的递归定义来总结上述三种项的形式:

t ::= x      (variable)
      λx.t   (abstraction)
      t t    (application)

抽象语法与具体语法

具体语法指程序员直接编写或阅读的代码,比如一段 C++ 代码 x = 1 + 2 * 3;

抽象语法则是更加本质的一种表示,采用抽象语法树 abstract syntax trees 的方式,更清楚地揭示语法中项和项之间关系,对于上面一串代码可能有如下抽象语法树 AST

    =
  /   \
x      +
     /   \
    1     *
        /   \
       2     3

lambda 项表示的是一棵语法树,即使我们写成线性形式(一行)

为了简化 lambda 演算,做出如下两个约定:

  • 左结合,意味着 x y t 等价于 (x y) t

  • 抽象 abstraction 的右部应当尽可能向右拓展(尽可能长),意味着 λx . λy . x y x 等价于 λx . (λy . ((x y) x))

作用域

还需要讨论变量 x 的作用域 scope,也就是 x 什么时候是“自由”的:对于 x,当 x 出现在 λx . tt 中时,我们认为 x 是被约束 bound 的,否则是自由的;我们称 λx 是一个绑定器 bindert 是这个绑定器的作用域 scope

一个不含自由变量的项成为封闭项(组合子 combinators),最简单的组合子,称为恒等函数,记作 id = λx . x

操作语义

在纯粹的 lambda 运算中,没有内置的常数,原始运算符等等,“运算”的唯一含义是将函数应用 application 于参数(同样也是函数)上

计算(上文所说的 application)的步骤为将左侧的 abstraction 中的约束变量替换成右侧的参数,记作:

(λx . t12) t2 → [x → t2] t12

[x → t2] 意为由 t2 代换在 t12 中所有自由出现的 x

比如: (λx . x) y 经计算之后为 y

由 Church 的定义,[x → t2] t12 称为一个可归约 reduce 表达式 redex reduciable expressions,并且根据上述操作进行计算的操作称为 beta 规约

下面给出了几个求值策略,用来确定项在下一步求值中如何激活这个可归约表达式

  • beta 规约 full beta-reduction:任何时刻都可以归约任何一个 redex,比如:

对于 (λx . x)((λx . x)(λz . (λx . x) z)) ≡ id(id(λz . id (z)))

可以从内向外归约 id(id(λz . id (z))) → id(id(λz . z)) → ... → λz . z

显然从外向内归约可以得到一样的结果

  • 常规顺序策略 normal order strategy:外面的 redex 最先归约

  • 按名调用策略 call by name strategy:和常规顺序策略类似,但是不允许在 abstraction 内部进行归约,同样以上文的例子为例,λz . id z 被认为不可进行归约,演算进行到 λz . id z 就停止了

  • 按值调用策略 call by value strategy: 最常用的策略;归约外层 redex,且一个 redex 会被归约仅当它的参数已经是一个值 value,即一个计算已经完成,已经不能被归约的形式,包括 lambda abstractions,numbers,booleans 等。

本书中采用按值调用策略

多参数 multiple arguments 与柯里化 Currying

上文提出的 lambda 演算没有给予多参数很好的支持,一种很优美的解决方法是利用高阶函数来达到这一效果,也就是函数柯里化

函数柯里化的主要思路是,将多参数函数转化为多个单参数的高阶函数,比如:

func(1, 2, 3) → func(1)(2)(3)

采用 lambda 语言书写:

f = λ(x, y) . s 写成 f = λx . λy . s,那么对于 f v w(λy . [x → v] s) w [y → w] [x → v] s

题外话,下面给出函数柯里化的一种 js 实现:

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

// 作者:shichuan
// 链接:https://juejin.cn/post/7215497169469685815
// 来源:稀土掘金
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Church 布尔式

定义项 trufls 如下所示:

tru = λt . λf . t 表示布尔值“真”

fls = λt . λf . f 表示布尔值“假”

那么就可以定义一个组合子 test b v w,具有类似于 if 的性质,当 btru 时返回第一个参数 v

定义:

test = λl . λm . λn . l m n

验证:

test tru v w
= (λl . λm . λn . l m n)(tru v w)
= tru v w
= (λt . λf . t)v w
= v

有意思的是,这个 tru 并不符合我们对“真”的直观感觉,但在 lambda 语言里却拥有“真”的运算性质

同样的可以定义 and = λb . λc . b c fls

序偶

pair = λf . λs . λb . b f s

fst = λp . p tru

snd = λp . p fls

验证过程略去

Church 数值

定义 Church 数值 c0, c1, c2

c0 = λs . λz . z

c1 = λs . λz . s z

c2 = λs . λz . s (s z)

etc

同样可以定义后继函数 scc = λn . λs . λz . s (n s z);后继函数在形式上很容易理解,通俗的说,对于一个给定的 Church 数值 n,代入两个参数 s z,再叠加一次 s

加法 plus = λm . λn . λs . λz . m s (n s z);通俗的说,m + n 即在 n 上做 m 次后继

同理,乘法 times = λm. λn . m (plus n) c0;数学上等价于 n + n + ... + n + 0

减法的定义要复杂一些,可以借用下面的前驱函数 prd

zz = pair c0 c0

ss = λp . pair (snd p) (plus c1 (snd p))

prd = λm . fst (m ss zz)

pFmxo0s.png

减法 sub = λm . λn . n prd m

丰富运算 enrich the calculus

既然布尔表达式和数值都能在这个 lambda 系统中进行表示和运算,那么理论上可以借助 lambda 进行任何程序的设计

用符号 λ 表示纯粹的 lambda 运算,λNB 表示无类型算术表达式 untyped arithmetic expression 的系统

pFmzMNt.png

pFmz19f.png

很容易实现二者之间的转换:

realbool = λb . b true false

churchbool = λb . if b then true else false

在高级一点的操作中也能实现这种转换:

realeq = λm . λn . (equal m n) true false

realnat = λm . m (λx . succ x) 0

使用原始布尔值和数(指 λNB系统)的理由主要与求值策略 evaluation order 有关;前文说过本书主要采用按值调用策略 call by value strategy,考虑对于项 scc c1,这个结果期望是 c2,然而事实上首先会得到一个可归约表达式,经过一到两步归约得到 c2,这一行为在按值调用中是不允许的;这样的话,运算及其结果就会异常复杂

递归

不能继续进行求值的一个项称为一个范式,然而对于下面的 omega,它并不能被求值成一个范式:

omega = (λx . x x) (λx . x x)

omega 进行一步归约会产生另一个 omegaomega 有一个有用的推广,称为不动点组合子 fixed-point combinator

fix=λf.(λx.f (λy.x x y)) (λx.f (λy.x x y)); 或者更简单的按名调用 Y = λf . (λx. f(x x)) (λx. f(x x))

使用方法是:

f = ⟨body containing f⟩ # 对于一个递归函数 f
g = λf.⟨body containing f⟩ # 定义 f 的初步 lambda 形式
h = fix g # 得到递归 lambda 函数

比如对于阶乘函数 factorial 有如下式子:

f = if realeq n c0 then c0
    else times n (if realeq n-1 c0 then c0
                  else times n-1 (...))

g = λf . λn . if realeq n c0 then c1 else times n f (prd n)

h = fix g

前面的 factorial 使用的是 if 而不是 test,是因为在 call-by-value 下,如果要对 test 进行 evaluate,则必须要求出其两个分支的内容后才能进一步 reduce,而这样会导致 diverge

https://roife.github.io/posts/tapl-05/

语法

下面将更加详细地确定 lambda 语法的演算和语义

定义:设 V 是一个变量名的可数集合,项的集合是最小的集合 T

  • 对每个 x ∈ Vx ∈ T

  • 如果 t1 ∈ T 且 x ∈ V,那么 λx . t1 ∈ T

  • 如果 t1 ∈ T 且 t2 ∈ T,那么 t1 t2 ∈ T

定义:项 t 的自由变量集合记为 FV(t)

  • FV(x) = x

  • FV(λx . t1) = FV(t1) - x

  • FV(t1 t2) = FV(t1) + FV(t2)

代换

上面给出了一个简单的代换 (λx . t1) t2 = [x → t2]t1

形式上的定义一个函数 [x → s],有这样一个特殊的例子:

[x → y](λx . x) = λx . y

这个转换将一个恒等函数变换为了一个常量函数

  • 第一个错误:x 本来已被界定,代换后变为自由的

[x → z](λz . x) = λz . z

这个转换将一个常量函数变换为了一个恒等函数

  • 第二个错误:x 本来是自由变量,代换后变成界定的(变量捕捉)

然后我们稍作修改,可以得到一个几乎正确的定义:

pFnn4PO.png

新定义无法操作 [x → y z](λy . x y),问题在于 y

解决这个问题的共同方法是假定项在受界定变量的重新命名下是相同的(在 alpha 转换下是相同的)

操作语义

pFmzMNt.png

这个语义隐含地包含了求值顺序 E-App1 -> E-App2 -> E-AppABS

posted @ 2024-01-25 20:39  sysss  阅读(58)  评论(0)    收藏  举报