为什么不用词法封闭的`define`来实现`let`?

时间:2014-03-21 00:05:18

标签: scheme lisp

我一直在使用lisp-family语言多年,感觉我对它们有很好的把握。我现在正在编写自己的lisp(当然是时尚),但几乎完全避免重新实现Scheme,Common Lisp和朋友使用的相同模式。我总觉得奇怪的一件事是letletrecfletlabelslet* ......)的所有变体。

说出过去没有带来的遗产"实施lisp,我希望能够写出如下内容:

(let ((x 7)
      (y 8)
      (z (* x y)))
  (str "x * y * z = " (* x y z)))

同样我也希望能够写下来:

(let ((odd? (lambda (n) (if (zero? n) false (even? (dec n)))))
      (even? (lambda (n) (if (zero? n) true (odd? (dec n)))))
  (odd? 7))

我可以通过在lambda体内使用define来非常有效地实现这两种变体。请原谅clojure-scheme-hybrid代码:

(defmacro let (bindings & body)
  `((lambda ()
      ,@(map (lambda (name-value) (cons 'define name-value)) bindings)
      ,@body)))

因为这只是在lambda体内的封闭环境中引入了绑定,所以相互递归可以起作用,变量可以依赖于先前定义的存在。同样,因为lambda会关闭创建它的环境,所以扩展(define x expression) (define y expression)会按预期工作。

那么为什么有许多明确的形式会使问题复杂化?我忽略了什么吗?我已经实现了我自己的let,如上所示,它似乎在所有情况下都完全符合预期。这也降低了嵌套函数应用程序的成本开销,例如let*

2 个答案:

答案 0 :(得分:8)

内部定义由R5RS定义(har har)以使用letrec,R6RS和R7RS定义使用letrec*。您所描述的行为恰恰是letrec*

但是,在某些情况下,您希望使用外部绑定,并且您不希望内部绑定在定义期间将它们遮挡。在这种情况下,letlet*letrecletrec*更合适。

这是什么意思?这是let提供letrec没有的外部范围的一种方式:

(let ((x 1)
      (y 2))
  (let ((x (+ x y))
        (y (- x y)))
    (format #t "x = ~a, y = ~a~%" x y)))

此处,在(+ x y)(- x y)表达式中,我们使用xy外部绑定。因此,内部x将为3,内部y将为-1。

使用let*类似,只是绑定是连续的:

(let ((x 1)
      (y 2))
  (let* ((x (+ x y))
         (y (- x y)))
    (format #t "x = ~a, y = ~a~%" x y)))

此处,内部x的评估与let案例相同,但内部y的定义将使用内部x而不是外部x y(但它仍使用外部y,因为内部x尚未绑定)。因此,内部y将为3,内部letrec将为1。

使用letrec*x时,所有外部绑定都将被同名的内部绑定所遮蔽,并且您无权访问外部y或{{ 1}}在那些情况下。此属性允许将它们用于自引用函数和/或数据结构。

letrecletrec*是相似的,除了letrec之外,所有值都先评估,然后同时绑定到末尾的变量;而对于letrec*,每个变量的值都是从左到右依次计算和绑定的。


为了演示四种let类型,我写了一个小的Racket宏,它允许你测试每个类型的行为:

#lang racket
(define-syntax test-let
  (syntax-rules ()
    ((_ let)
     (let ((x "outer x")
           (y "outer y")
           (p (lambda (x y label)
                (printf "~a: x = ~s, y = ~s~%" label x y))))
       (let ((before (p x y "before"))
             (x (begin
                  (p x y "during x")
                  "inner x"))
             (between (p x y "between"))
             (y (begin
                  (p x y "during y")
                  "inner y"))
             (after (p x y "after")))
         (p x y "body"))))))

测试结果:

> (test-let let)
before: x = "outer x", y = "outer y"
during x: x = "outer x", y = "outer y"
between: x = "outer x", y = "outer y"
during y: x = "outer x", y = "outer y"
after: x = "outer x", y = "outer y"
body: x = "inner x", y = "inner y"

> (test-let let*)
before: x = "outer x", y = "outer y"
during x: x = "outer x", y = "outer y"
between: x = "inner x", y = "outer y"
during y: x = "inner x", y = "outer y"
after: x = "inner x", y = "inner y"
body: x = "inner x", y = "inner y"

> (require rnrs/base-6)
> (test-let letrec)
before: x = #<undefined>, y = #<undefined>
during x: x = #<undefined>, y = #<undefined>
between: x = #<undefined>, y = #<undefined>
during y: x = #<undefined>, y = #<undefined>
after: x = #<undefined>, y = #<undefined>
body: x = "inner x", y = "inner y"

> (require rnrs/base-6)
> (test-let letrec*)
before: x = #<undefined>, y = #<undefined>
during x: x = #<undefined>, y = #<undefined>
between: x = "inner x", y = #<undefined>
during y: x = "inner x", y = #<undefined>
after: x = "inner x", y = "inner y"
body: x = "inner x", y = "inner y"

希望这会使let类型之间的差异非常明显。 : - )

答案 1 :(得分:2)

程序中的

define(或者,因为它是相同的)是一个letrec,直到R5RS和来自R6RS的letrec*和当前的R7RS。你可以使用letrec*做任何事情,除了掩盖你自己的变量(它们变得未定义)。

(let ((x 10))
  (letrec ((x x))
     x)) ;; returns some undefined value

变为

(let ((x 10))
  (let ((x 'undefined))
     (let ((tmpx x)) ;; notice x is already shadowed by the new binding
        (set! x tmpx)
        x))) ;; returns some undefined value

原因是因为变量的评估需要在已定义变量的情况下完成,以便可以递归。您的相互递归odd?取决于它。虽然两个以相同的方式将新x设置为旧x,因为它们从不同时存在。

您可能知道(let ((x 10) (y 20)) ...)只是匿名过程调用((lambda (x y) ...) 10 20)的语法糖,let*是嵌套的(let* ((x 10) (y 20)) ...) => (let ((x 10)) (let ((y 20)) ... )))。命名letletrec,而letrec*几乎嵌套letrec(所有变量都是在未初始化的值中生成的,所以所有级别都可以引用它们)

如果你想绑定10个变量并且可以在一个闭包中完成它,那么使用define执行它时最便宜的是引入20个过程调用和10个突变。这可能是现代翻译人员今天无法管理的,但是当它被发明时,Scheme并没有表现出那么好的表现,如果它只有letrec*

那么会更糟糕。