Scheme语言中已有let绑定语法,为什么还需要设计letrec?
let 和 letrec 的核心差异
你遇到的let能正常运行的情况属于巧合,本质是lambda表达式的延迟求值特性掩盖了二者的作用域差异,二者的核心区别在于绑定的作用域生效时机:
let的执行逻辑:- 先依次求值所有绑定的右值表达式,此时绑定的变量名还未加入当前作用域,右值中引用的同名变量会指向外部作用域的定义
- 所有右值求值完成后,再将变量名和对应值绑定,加入当前作用域
- 执行
let的主体代码
letrec的执行逻辑:- 先将所有绑定的变量名加入当前
letrec块的作用域,初始为未初始化状态 - 再依次求值所有绑定的右值表达式,求值过程中可以引用同批次的其他变量名
- 执行
letrec的主体代码
- 先将所有绑定的变量名加入当前
你的示例能跑的原因
你示例中的右值都是lambda表达式,lambda内部引用的even?、odd?不会在定义lambda时立刻解析,只有调用lambda时才会查找变量。等你调用(even? 88)时,已经进入了主体代码执行阶段,不管是let还是letrec的变量都已经完成绑定,所以刚好能得到正确结果。
如果修改一下测试场景,let的问题会立刻暴露:
;; 外部先定义一个永远返回#t的odd? (define odd? (lambda (n) #t)) ;; 用let的版本 (let ((even? (lambda (n) (if (zero? n) #t (odd? (- n 1))))) (odd? (lambda (n) (if (zero? n) #f (even? (- n 1)))))) (even? 3)) ;; 预期结果是#f,实际let版本返回#t:因为even?的lambda捕获的是外部的odd?
换成letrec的版本则会返回正确的#f,因为lambda捕获的是当前letrec块内的odd?绑定。
letrec的实际作用
- 支持互递归函数定义:保证同批次绑定的多个函数之间可以互相引用,不会意外调用外部作用域的同名变量
- 支持自递归函数定义:不需要提前在外部定义占位符,就可以直接在绑定内引用自身,比如:
;; 只有letrec能正常运行返回120,let版本会报fact未绑定错误(如果外部没有fact定义) (letrec ((fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))) (fact 5)) - 语义更明确:明确表示当前块内的绑定是递归/互递归引用的,不会污染外部作用域。
内容的提问来源于stack exchange,提问作者Flux
相关产品推荐
相关产品推荐

