230
社区成员




substitution
在实现 lambda 演算的 eval
函数中,需要 App
相当于函数的应用,其中就是将对应的绑定变量进行替换,这个替换过程就是 substitution
任务中要求实现 N[v/x]
就是把 N
里面的 x
用 v
来替换掉,N
是一个 lambda 表达式,因此可以针对表达式的分类情况进行枚举,逐一执行
// expr2[expr1/s] 在表达式2中 用 表达式1 把 s 给替换掉
// s, expr2 一般是一个 lambda fn
// 这个过程 感觉有点像 贝塔规约
let rec subst = (s, expr1, expr2) => {
// bind s and expr1
let substitution = subst(s, expr1)
// 根据 expr2 的情况枚举处理
switch expr2 {
| Var(ss) if ss === s => expr1 // 找到替换的目标 则 Var(ss) 换成 expor1
| Var(_) => expr2 // 没得换 直接返回 expr2
| Fn(ss, e) if ss !== s => Fn(ss, substitution(e)) // 如果 expr2 是个函数定义 并且参数没有和要替换的相同 替换 body 部份 并且保留参数 ss
| Fn(_, _) => expr2 // 要替换的参数 s 由于内部 expr2 遮蔽了,所以也换不得;同名但是不同含义
| App(a, b) => App(substitution(a), substitution(b)) // a,b 递归处理即可
}
}
完整代码: lec2 task1 Implement the substitution function
N[M/x]
中 M
有自由变量的情况在 substitution
中,如果 M 有自由变量的话,就有一些麻烦的事情出现
比如 N = λx.xy
, M = λa.ax
, 如果要做的话N[M/y]
按照 任务一 的实现,就会变成 λx.x(λa.ax),这个时候,加粗的 x 的含义就很尴尬,它其实不是原来的绑定变量,但是现在却被变成了和参数x绑定起来了.
所以在做这个处理的时候,我们要考虑 N
中是否有自由变量会和原本的参数名相同,如果相同的话,就会造成这种歧义.为了避免相同,我们可以根据 阿尔法 规则,先把 N
中的 x
换个不在 M
中自由变量里面的“名字”.
所以第一步,我们需要能够获得一个 lambda 表达式中,自由变量列表的途径: getFreeVar(e: lambda): list<string>
,然后处理 Fn
的情况,进行分类讨论
// 从一个 expr 中得到自由变量列表
let getFreeVar = expr => {
let rec go = (ans, expr) => {
switch expr {
| Var(a) => list{a, ...ans}
| Fn(arg, body) => go(ans, body)->Belt.List.keep(it => it !== arg) // 排除掉绑定变量 arg
| App(fn, param) => go(go(ans, fn), param)
}
}
go(list{}, expr)
}
// 返回一个新的标识符
// getNewSymbol(i) !== i
let getNewSymbol = i => i ++ "_"
subst 的实现变化如下,主要是对 Fn 分支的变更
let rec subst = (s, expr1, expr2) => {
switch expr2 {
| Var(ss) if ss === s => expr1 // 找到替换的目标 则 Var(ss) 换成 expor1
| Var(_) => expr2 // 没得换 直接返回 expor2
- | Fn(ss, e) if ss !== s => Fn(ss, substitution(e)) // 如果 expr2 是个函数定义 并且参数没有和要替换的相同 替换 body 部份 并且保留参数 ss
- | Fn(_, _) => expr2 // 要替换的参数 s 由于内部 expr2 遮蔽了,所以也换不得;同名但是不同含义
+ | Fn(arg, body) =>
+ // Think about how substitution works on arbitrary terms, i.e. N[M/x] where M could contain free variables.
+ if arg === s {
+ // 否则如果绑定变量和当前替换的 s 相等的话,产生遮蔽的效果,替换不了了 原样返回
+ expr2
+ } else if getFreeVar(expr1)->Belt.List.has(arg, String.equal) {
+ // 如果 body 中绑定变量 arg 和 expr1 中有命名冲突需要进行处理
+ let symbol = getNewSymbol(arg)
+ let newBody = subst(arg, Var(symbol), body)
+ // 基于新的
+ Fn(symbol, substitution(newBody))
+ } else if arg !== s {
+ // 如果绑定变量和当前替换的 s 不相等的话,直接穿透 在 body 继续替换
+ Fn(arg, substitution(body))
+ } else {
+ assert false
+ }
| App(a, b) => App(substitution(a), substitution(b)) // a,b 递归处理即可
}
}
变更提交: lec2 task2 subst works on arbitrary terms
这里主要是补充 lec2-code 中的 src/Nat.res
中关于 Peano 和 Church 两种形式的 加法和乘法
// a + b
let rec peano_add = (n: nat, m: nat): nat => {
switch n {
| Z => m // 0 + k === k
| S(n') => peano_add(n', peano_succ(m)) // n + m <=> (n - 1) + (m + 1)
}
}
let rec peano_mul = (n: nat, m: nat): nat => {
switch n {
| Z => Z
| S(n') => // n * m <=> ((n - 1) + 1) * m <=> (n - 1) * m + m
peano_add(peano_mul(n', m), m)
}
}
// add = λnmfx.nf(mfx)
let church_add = (n: cnum<_>, m: cnum<_>): cnum<_> => (f, x) => n(f, m(f, x))
// let constTrue = (x, _) => x
// let constFase = (_, y) => y
// let ifThenElse = (x) => x
// isZero(church_zero) => T
// isZero(church_one) => F
// let isZero = (n: cnum<_>) => n((_) => constFase, constTrue)
// mul = λnmfx.f^(n*m)x mul m n => 对 f 迭代 n * m 次
let church_mul = (n: cnum<_>, m: cnum<_>): cnum<_> => {
// n * m
// <=> m + m + ... + m (一共 n 个 m 相加)
// <=> (...((m) + m) + m) + ...)
// <=> (...((0 + m) + m) + m) + ...)
// n => λnfx.f^(n)x 对 f 迭代 n 次
// m => λmfx.f^(m)x 对 f 迭代 m 次
// 记 F = λx.f^m 把 f apply 到 m 上 即 m(f)
// mulBody = F^(n)x 把 F appy 到 n 上,即 n(F)
f => n(/* F */ m(f))
}
疑问点记录: church_mul
本来是想通过课件中 lec2-part2.pdf#20 里面的乘法定义,用递归、 pred
、isZero
组合来实现,但是失败了 pred(n)
是无法调用的,类型错误
这里是想用 任务一/二 中我们自己写的 lambda ast 来模拟 church number, 这个时候就可以用 课件 20 页的乘法定义来表达乘法了
open Lambda
// λfx.x
let church_zero = Fn("f", Fn("x", Var("x")))
// λfx.fx
let church_one = Fn("f", Fn("x", App(Var("f"), Var("x"))))
// λfx.f(fx)
let church_two = Fn("f", Fn("x", App(Var("f"), App(Var("f"), Var("x")))))
// λfx.f(f(fx))
let church_three = Fn("f", Fn("x", App(Var("f"), App(Var("f"), App(Var("f"), Var("x"))))))
let constTrue = Fn("x", Fn("y", Var("x")))
let constFalse = Fn("x", Fn("y", Var("y")))
let ifThenElse = Fn("x", Var("x"))
// λnfx.f(nfx)
// succ zero => (λnfx.f(nfx))(λfx.x) => λfx.f((λfx.x)fx) => λfx.fx => one
// succ one => (λnfx.f(nfx))(λfx.fx) => λfx.f((λfx.fx)fx) => λfx.f(fx)
let successor = Fn("n", Fn("f", Fn("x", App(Var("f"), App(App(Var("n"), Var("f")), Var("x"))))))
eval(App(successor, church_two))->show->Js.log2("succ church_two", _)
// λnmfx.nf(mfx)
// add one two => λfx.(one)f((two)fx) => λfx.(λx.fx)(f(fx)) => λfx.f(f(fx))
let add = Fn(
"n",
Fn("m", Fn("f", Fn("x", App(App(Var("n"), Var("f")), App(App(Var("m"), Var("f")), Var("x")))))),
)
eval(App(App(add, church_one), church_two))->show->Js.log2("add one two", _)
let iszero = Fn("n", App(App(Var("n"), Fn("z", constFalse)), constTrue))
eval(App(iszero, church_one))->show->Js.log2("is zero", _)
let pair = Fn("x", Fn("y", Fn("z", App(App(Var("z"), Var("x")), Var("y")))))
let fst = Fn("p", App(Var("p"), constTrue))
let snd = Fn("p", App(Var("p"), constFalse))
eval(App(fst, App(App(pair, church_one), church_two)))->show->Js.log2("fst pair one two", _)
let pred = {
let f = Fn(
"p",
{
let secondP = App(snd, Var("p"))
App(App(pair, secondP), App(successor, secondP))
},
)
let pc0 = App(App(pair, church_zero), church_zero)
Fn("n", App(fst, App(App(Var("n"), f), pc0)))
}
// λf.λx.f ((λf.λx.f ((λf.λx.x) f x)) f x)
// <=> λf.λx.f(fx)
// <=> church_two
eval(App(pred, church_three))->show->Js.log2("pred lambda", _)
let mul = {
let yCombinator = {
Fn(
"f",
App(
Fn("x", App(Var("f"), App(Var("x"), Var("x")))),
App(Var("f"), App(Var("x"), Var("x"))),
),
)
}
let f = {
Fn(
"f",
Fn(
"n",
Fn(
"m",
App(
// if(n=0) then 0
App(App(ifThenElse, App(iszero, Var("n"))), church_zero),
// else (m + f(n - 1)m)
App(
App(add, Var("m")),
{
let predN = App(pred, Var("n"))
App(App(Var("f"), predN), Var("m"))
},
),
),
),
),
)
}
App(yCombinator, f)
}
let churchNumberToInt = e => {
let rec go = (ans, exp) => {
switch exp {
| Fn(_, _) as n => if n == church_zero {
ans
} else {
go(ans + 1, eval(App(pred, n)))
}
| _ => assert false
}
}
go(0, eval(e))
}
churchNumberToInt(App(App(add, church_two), church_three))
->Js.Int.toString
->Js.log2("add 2 3 is", _)
churchNumberToInt(App(App(add, church_three), church_two))
->Js.Int.toString
->Js.log2("add 3 2 is", _)
churchNumberToInt(App(pred, church_three))->Js.Int.toString->Js.log2("pred 3 is", _)
lambda 演算是颠覆我对函数式编程范式的认知的重大知识点.从前以为封装过程称为函数,抽得越细越多,就是在使用函数式编程.
但是如果不是有邱奇数这样的例子和万物都是表达式/函数,我是体会不到的它和过程式的代码也就是指令的不同之处的,最直接的一点就是赋值和let binding这样的区别.
之前曾在公司小组里面交流讨论过说,函数式中强调数据的不可变,倒不如说是因为所使用的编码语言(js / java / ts) 为了达到函数式中数学表达的“正确性”而所做的“约束”.个人理解,如果一个语言本身就是一个纯函数式语言,它是不用去强调不可变的数据操作的,其实都是一些“符号”,做的事情是在一些既定规则下,做符号的替换.
鼓励一下交作业的好学生! 请看红包!