在Scheme中使用“let over lambda”形式定义多个本地函数

时间:2012-10-31 00:18:41

标签: lambda scheme sicp lexical-scope

我很想知道在Scheme中定义多个可以相互调用的词法范围函数。在SICP中工作,我使用块结构生成了以下函数来解决练习1.8(使用牛顿方法计算立方根):

(define (cbrt x)
  (define (good-enough? guess prev-guess)
    (< (/ (abs (- guess prev-guess))
          guess)
       0.001))
  (define (improve guess)
    (/ (+ (/ x (square guess))
          (* 2 guess))
       3))
  (define (cbrt-iter guess prev-guess)
    (if (good-enough? guess prev-guess)
        guess
        (cbrt-iter (improve guess)
                   guess)))
  (cbrt-iter 1.0 0.0))

这很好用,但它让我想知道Scheme(也许是Common Lisp)如何使用词法作用域和let形式来处理这个场景。我尝试使用let使用以下kludgy代码实现它:

(define (cbrt x)
  (let ((calc-cbrt
         (lambda (guess prev-guess)
           (let ((good-enough?
                  (lambda (guess prev-guess)
                    (< (/ (abs (- guess prev-guess))
                          guess)
                       0.001))))
             (good-enough? guess prev-guess))
           (let ((improve
                  (lambda (guess)
                    (/ (+ (/ x (square guess))
                          (* 2 guess))
                       3))))
             (improve guess))
           (let ((cbrt-iter
                  (lambda (guess prev-guess)
                    (if (good-enough? guess prev-guess)
                        guess
                        (cbrt-iter (improve guess)
                                   guess)))))
             (cbrt-iter 1.0 0.0)))))
    (calc-cbrt 1.0 0.0)))

我在下面看到的问题是当cbrt-iter尝试调用good-enough?程序时。由于good-enough?过程仅在第一个嵌套let块的范围内,cbrt-iter无法访问它。似乎可以通过将cbrt-iter函数嵌套在let的封闭good-enough内来解决这个问题,但这似乎也非常麻烦和笨拙。

在这种情况下,define形式做什么是不同的? define表单是否扩展为lambda表达式而不是“let over lambda”表单(我记得在Little Schemer书中使用((lambda (x) x x) (lambda (y) ...))形式进行了类似的操作,但我不是确定这将如何工作)。另外,通过比较,Common Lisp如何处理这种情况 - 是否可以使用词法范围defun

1 个答案:

答案 0 :(得分:1)

首先,您无需引入新程序calc-cbrt - 您只需拨打calc-iter即可。

其次,definelet的含义完全不同。 Define 将定义安装到本地范围中,如您的示例所示。但是,let表达式只是lambda表达式的语法糖(有关详细信息,请参阅SICP第1.3节)。结果(并且如您所述),通过(let (<decl1> ...) <body>)声明的变量仅在<body>内可见。因此,(let <decls1> <body1>) (let <decls2> <body2>) ...的模式不起作用,因为没有一个定义会“存活”到其他范围内。

所以,我们应该这样写:

(define (cbrt x)
   (let ((good-enough? (lambda ...))
         (improve (lambda ...))
         (cbrt-iter (lambda ...)))
     (cbrt-iter 1.0 0.0)))

现在,至少,对cbrt-iter的调用可以看到cbrt-iter的定义。

但仍有问题。在我们评估(cbrt-iter 1.0 0.0)时,我们会评估cbrt-iter的正文,其中guessprev-guess取值1.0和0.0。但是,在cbrt-iter的正文中,变量improvegood-enough?不在范围内。

您可能想要使用嵌套的let,这通常是一个不错的选择:

(define (cbrt x)
   (let ((good-enough? (lambda ...))
         (improve (lambda ...)))
     (let ((cbrt-iter (lambda ...)))
       (cbrt-iter 1.0 0.0))))

问题在于cbrt-iter需要调用自己,但是在内部let的主体之前它不在范围内!

这里的解决方案是使用letrec,它类似于let,但使新绑定在所有声明和正文中都可见:

(define (cbrt x)
   (let ((good-enough? (lambda ...))
         (improve (lambda ...)))
     (letrec ((cbrt-iter (lambda ...)))
       (cbrt-iter 1.0 0.0))))

我们甚至可以使用letrec来创建相互递归的过程,就像使用define一样。

不幸的是,我需要一些时间来解释letrecdefine实际上是如何工作的,但这是秘密:它们都在内部使用变异来创建圆形环境数据结构,允许递归。 (还有一种方法可以仅使用lambda来创建递归,称为 Y组合器,但它相当复杂且效率低下。)

幸运的是,所有这些秘密将在第3章和第4章中公布!

从另一个角度来看,你可以看看Brown University's online PL class,这基本上直接讨论了这个主题(虽然它省略了define),但我发现SICP更能强迫你理解有时会创建复杂的环境结构。