231
社区成员
发帖
与我相关
我的任务
分享
本次作业基于课程信息给出的示例代码实现。
补全 src/Name.res
中 Debru.eval
函数:
// Homework: implement the complete interpreter
let rec eval = (t: lambda) => {
switch t {
| Var(_) => t // ⊥ 值
| Fn(f) => Fn(eval(f)) // 尽最大努力地求值
| App(a, b) =>
switch eval(a) {
| Fn(f) => eval(subst(f, 0, eval(b))) // 注意,替换后再递归求值
| a => App(a, eval(b)) // 尽最大努力地求值
}
}
}
更新:图灵等价的完备求值实现,差别在于对
Fn(f)
的处理上,定制的bind
函数可以保证停机,从而避免过早陷入死循环
let rec bind = (t: lambda) => { switch t { | Var(_) => t | Fn(f) => Fn(bind(f)) | App(a, b) => switch bind(a) { | Fn(f) => subst(f, 0, bind(b)) // 唯一的区别:不会对替换结果进一步求值,从而保证一定能停机 | a => App(a, bind(b)) } } } let rec eval = (t: lambda) => { switch t { | Var(_) => t | Fn(f) => Fn(bind(f)) | App(a, b) => switch eval(a) { | Fn(f) => eval(subst(f, 0, eval(b))) // 注意,替换后再递归求值 | a => App(a, eval(b)) // 尽最大努力地求值 } } }
本次实现采取了“尽最大努力”的求值策略,对于未绑定变量不会抛出异常,而是返回“⊥”(即原封不动返回输入)。
添加一个函数用于测试输出:
let rec print_lambda = (t:lambda) => {
switch t {
| Var(j) => Belt.Int.toString(j)
| Fn(b) => "(\\. " ++ print_lambda(b) ++ ")"
| App(a, b) => print_lambda(a) ++ " " ++ print_lambda(b)
}
}
测试代码:
// (\x. x x) (\x. x) = \x.x
// (\. 0 0) (\. 0) = \. 0
let test_1 = App(Fn(App(Var(0), Var(0))), Fn(Var(0)))
Js.Console.log(print_lambda(eval(test_1)))
// (\y. y (\x.x \x.x)) = (\y. y (\x.x))
// (\. 0 (\.0 \.0)) = (\. 0 (\.0))
let test_2 = Fn(App(Var(0), App(Fn(Var(0)), Fn(Var(0)))))
Js.Console.log(print_lambda(eval(test_2)))
// \y.(\x.x y) \y.(\x.x y) = \x.(x \y.(\x.x y))
// (\. \. 0 1) (\. \. 0 1) = \. 0 (\. \. 0 1)
let test_3 = App(
Fn(Fn(App(Var(0), Var(1)))),
Fn(Fn(App(Var(0), Var(1)))),
)
Js.Console.log(print_lambda(eval(test_3)))
运行输出如下:
(\. 0)
(\. 0 (\. 0))
(\. 0 (\. (\. 0 1)))
作业2的实现我采用了偷懒的做法:由于 Let 等价于一个函数抽象操作加一个函数应用操作(例如let x=1 in x+y
等价于 (\x. x+y) 1
),所以可以将作业2的AST转化为作业1的AST然后用作业1的函数进行计算。
补充 src/Name.res
中 DebruLet.eval
函数定义如下:
// Homework: support let
module DebruLet = {
type rec lambda =
| Var (int)
| App (lambda, lambda)
| Fn (lambda)
| Let (lambda, lambda) // let x=1 in x+y 等价于 (\x. x+y) 1
let rec eval_cheat = (t: lambda) => {
let trans = switch t {
| Var(i) => Debru.Var(i)
| App(a, b) => Debru.App(eval_cheat(a), eval_cheat(b))
| Fn(a) => Debru.Fn(eval_cheat(a))
| Let(a, b) => Debru.App(Debru.Fn(eval_cheat(b)), eval_cheat(a))
}
Debru.eval(trans)
}
}
测试代码:
// let x=(\x. x) in (x x) = \x.x
let test_1 = Let(Fn(Var(0)), App(Var(0), Var(0)))
Js.Console.log(Debru.print_lambda(eval_cheat(test_1)))
运行输出如下:
(\. 0)
如果要是不偷这个懒要怎么做呢?那就得把 Debru
模块里的 shift
函数 subst
函数再复制粘贴一遍,添加关于 Let 的 switch 情况(只有一点点代码),然后 eval 函数中的 swtich 的 Let 分支就写成:
let rec eval = (t:lambda) => {
switch t {
...
| Let(a,b) => eval(subst(eval(a), 0, eval(b)))
}
}
这种过分积极的求值会导致一个问题,如果函数体里包含死循环,那么求值时就会提前停机,这会导致Y组合子无法使用。
例如:(\f. (\.x f x x) (\.x f x x)) (\x y. (\z. z))
的求值结果显然为 \z. z
,但是使用上面的尽最大努力求值,就会在Y组合子处死循环导致无法停机。
那么问题就是:这种积极求值的方法是不是图灵等价的呢?