揭秘语言实现:从 7 行 Lambda Calculus 到函数式解释器
实现一种编程语言通常被视为一项艰巨的任务,只有编译器工程师和博士才能胜任。然而,其核心过程是对计算本质的理解练习。通过将语言剥离到其绝对本质,我们可以看到数学符号与工作解释器之间的差距其实小得惊人。
本指南探讨了基于 lambda calculus 的函数式、图灵完备语言的实现,并利用了在 Structure and Interpretation of Computer Programs 等经典著作中发现的可扩展 eval/apply 设计模式。
基础:Lambda Calculus
由 Alonzo Church 在 20 世纪初开发,lambda calculus 是一种用于表达计算的极简系统。它不是现代意义上的“编程语言”——它缺乏内置的数字、布尔值或 I/O——但它是图灵完备的。这意味着任何由图灵机可计算的函数都可以在 lambda calculus 中表达。
该系统仅依赖三种类型的表达式:
- 变量引用 (Variable References):指代某个值的名称。
- 匿名函数 (Abstractions):写作
(λ v . e),其中v是参数,e是返回的表达式。 - 函数调用 (Applications):写作
(f e),其中函数f被应用于参数e。
实现图灵完备性
在没有显式循环或递归的情况下,这样一个稀疏的系统如何处理复杂的逻辑?lambda calculus 采用了两种主要的“技巧”:Church encodings(将整数和布尔值等数据表示为函数)和 Y Combinator(在匿名函数中实现递归)。
该系统力量的一个显著例子是 "Omega" 程序:((λ f . (f f)) (λ f . (f f)))。这个程序永远不会终止,证明了 lambda calculus 可以表达任何通用语言中存在的无限循环。
7 行解释器
使用 R5RS Scheme,我们可以在仅用七行代码的情况下为 lambda calculus 实现一个指称性解释器。Scheme 在这里是一个理想的选择,因为其 read 函数会自动处理词法分析并将表达式解析为 s-expressions。
; eval takes an expression and an environment to a value
(define (eval e env) (cond
((symbol? e) (cadr (assq e env)))
((eq? (car e) 'λ) (cons e env))
(else (apply (eval (car e) env) (eval (cadr e) env)))))
; apply takes a function and an argument to a value
(define (apply f x)
(eval (cddr (car f)) (cons (list (cadr (car f)) x) (cdr f))))
; read and parse stdin, then evaluate:
(display (eval (read) '())) (newline)
工作原理:Eval/Apply 模式
该架构依赖于两个相互递归的函数:
eval:将Expression和Environment映射到Value。apply:将一个Value(函数)和另一个Value(参数)映射到新的Value。
在此实现中,使用 Closure 来“封闭”一个开放项。它将 lambda 表达式与定义其自由变量的环境配对,确保函数无论在何处被调用,都能携带其上下文。
扩展规模:构建更丰富的语言
虽然 7 行版本只是一个概念验证,但 eval/apply 模式是可扩展的。通过扩展 eval 函数以处理更多表达式形式,我们可以构建功能更强大的语言。在 Racket 中进行 100 行规模的实现可以引入:
- 常量 (Constants):数值和布尔字面量。
- 原语操作 (Primitive Operations):加法、减法和比较。
- 控制流 (Control Flow):
if条件判断和begin顺序执行。 - 变量绑定 (Variable Bindings):用于局部绑定的
let和用于递归绑定的letrec。 - 状态 (State):用于变量变异的
set!。
通过将语法设计(代码如何编写)与语义设计(逻辑如何求值)分离,开发者可以尝试不同的语法,只需构建一个输出 s-expressions 以供解释器处理的解析器即可。
技术视角与评论
实现这些概念通常会引发关于函数式极简主义实用性的辩论。有人认为,在类 Lisp 语言中实现 lambda calculus 实际上是在“用 Lisp 实现 Lisp”,这表明其简洁性是宿主语言的固有特性,而非目标语言的简单性。
然而,其他人指出,这项练习不仅仅是为了代码——它是为了揭示计算的“魔法”。正如一位贡献者所言:
"构建一个带有 LED 和逻辑门电路的 ALU 应该成为任何课程的一部分... 人们应该意识到,是的,它很复杂,但它也并非魔法。"
此外,lambda calculus 甚至可以扩展到高层解释器之外。在 Binary Lambda Calculus (BLC) 中,项被表示为比特,这允许实现极其紧凑的自解释器——有时甚至比这里提供的 7 行 Scheme 代码还要小。
结论
无论你是在为了乐趣构建一个玩具语言,还是在设计生产级别的编译器,从 lambda calculus 开始都能让你对值、环境和函数是如何交互的提供根本性的理解。eval/apply 模式作为一个可扩展的蓝图,证明了即使是最复杂的编程语言,也是建立在少数几个简单的、递归的基础之上。