35

因此,我花了很多时间阅读和重新阅读The Little Schemer中第 9 章的结尾,其中为该length功能开发了应用 Y 组合器。我认为我的困惑归结为一个对比两个版本的长度的声明(在组合器被分解之前):

A:
  ((lambda (mk-length)
     (mk-length mk-length))
   (lambda (mk-length)
     (lambda (l)
       (cond
         ((null? l) 0 )
         (else (add1
                ((mk-length mk-length)
                 (cdr l))))))))

B:
((lambda (mk-length)
      (mk-length mk-length))
    (lambda (mk-length)
      ((lambda (length)
         (lambda (l)
           (cond
             ((null? l) 0)
             (else (add1 (length (cdr l)))))))
       (mk-length mk-length))))

第 170 页(第 4 版)指出,A

当我们将它应用于参数时返回一个函数

而乙

不返回函数

从而产生自我应用的无限倒退。我被这件事难住了。如果 B 受到这个问题的困扰,我看不出 A 是如何避免的。

4

2 回答 2

39

好问题。为了那些没有正常运行 DrRacket 安装(包括我自己)的人的利益,我将尝试回答它。

首先,让我们使用一些健全的(短)变量名称,人眼/头脑可以轻松跟踪:

((lambda (h)     ; A.   
     (h h))            ; apply h to h
 (lambda (g)
     (lambda (lst)
       (if (null? lst) 0
         (add1 
               ((g g) (cdr lst)))))))

第一个 lambda 项是所谓的little omega 或U组合器。当应用于某物时,它会导致该术语的自我应用。因此以上等价于

(let ((h (lambda (g)
           (lambda (lst)
             (if (null? lst) 0
               (add1 ((g g) (cdr lst))))))))
  (h h))

h应用于 时h,会形成新的绑定:

(let ((h (lambda (g)
           (lambda (lst)
             (if (null? lst) 0
               (add1 ((g g) (cdr lst))))))))
  (let ((g h))
    (lambda (lst)
            (if (null? lst) 0
              (add1 ((g g) (cdr lst)))))))

现在没有什么可以应用了,所以lambda返回了内部形式——连同let它上面的环境框架(即那些绑定)的隐藏链接。

lambda 表达式与其定义环境的这种配对称为闭包。对外界来说,它只是一个参数的另一个函数,lst. 目前没有更多的减少步骤可以在那里执行。

现在,当那个闭包——我们的list-length函数——将被调用时,执行最终将到达(g g)自我应用的点,并且将再次执行上述相同的归约步骤。但不是更早。


现在,那本书的作者想要得到Y组合器,所以他们对第一个表达式应用了一些代码转换,以某种方式安排自动执行自我应用程序(g g)——所以我们可以用普通的递归函数应用程序编写方式,,(f x)而不必像((g g) x)所有递归调用一样编写它:

((lambda (h)     ; B.
     (h h))            ; apply h to h
 (lambda (g)
   ((lambda (f)           ; 'f' to become bound to '(g g)',
      (lambda (lst) 
        (if (null? lst) 0
          (add1 (f (cdr lst))))))  ; here: (f x) instead of ((g g) x)!
    (g g))))                       ; (this is not quite right)

现在经过几个减少步骤,我们到达

(let ((h (lambda (g)
           ((lambda (f)    
              (lambda (lst) 
                (if (null? lst) 0
                  (add1 (f (cdr lst))))))
            (g g)))))
  (let ((g h))
    ((lambda (f)
       (lambda (lst)
            (if (null? lst) 0
              (add1 (f (cdr lst))))))
     (g g))))

这相当于

(let ((h (lambda (g)
           ((lambda (f)    
              (lambda (lst) 
                (if (null? lst) 0
                  (add1 (f (cdr lst))))))
            (g g)))))
  (let ((g h))
    (let ((f (g g)))           ; problem! (under applicative-order evaluation)
       (lambda (lst)
            (if (null? lst) 0
              (add1 (f (cdr lst))))))))

麻烦来了:在内部 lambda 甚至可以作为闭包返回给运行时系统之前,自我应用(g g)执行得太早了。我们只希望在调用闭包之后执行到lambda 表达式内的那个点时减少它。在创建闭包之前减少它是荒谬的。一个微妙的错误。:)

当然,由于gis bound to h(g g)被简化为(h h),我们又回到了我们开始的地方,h申请h. 循环播放。


作者当然知道这一点。他们也希望我们理解它。

所以罪魁祸首很简单——它是评估的应用顺序:在绑定由函数的形参及其参数的值形成之前评估参数。

那么,代码转换并不完全正确。它本来可以在不预先评估参数的正常顺序下工作。

这可以通过“ eta-expansion ”轻松解决,它将应用程序延迟到实际调用点:(lambda (x) ((g g) x))实际上是说:“在使用参数调用((g g) x)时调用x”。

这实际上是代码转换应该放在首位的:

((lambda (h)     ; C.
     (h h))            ; apply h to h
 (lambda (g)
   ((lambda (f)           ; 'f' to become bound to '(lambda (x) ((g g) x))',
      (lambda (lst) 
        (if (null? lst) 0
          (add1 (f (cdr lst))))))  ; here: (f x) instead of ((g g) x)
    (lambda (x) ((g g) x)))))

现在可以执行下一个缩减步骤:

(let ((h (lambda (g)
           ((lambda (f)    
              (lambda (lst) 
                (if (null? lst) 0
                  (add1 (f (cdr lst))))))
            (lambda (x) ((g g) x))))))
  (let ((g h))
    (let ((f (lambda (x) ((g g) x))))       ; here it's OK
      (lambda (lst)
            (if (null? lst) 0
              (add1 (f (cdr lst))))))))

并且闭包(lambda (lst) ...)在没有问题的情况下形成并返回,当(f (cdr lst))被调用时(在闭包内),它会减少到((g g) (cdr lst))我们想要的样子。


最后,我们注意到(lambda (f) (lambda (lst ...))表达式 inC.不依赖于任何hand g。所以我们可以把它拿出来,让它成为一个论点,然后剩下...... Y组合子:

( ( (lambda (rec)            ; D.
      ( (lambda (h) (h h))  
        (lambda (g)
          (rec  (lambda (x) ((g g) x))))))   ; applicative-order Y combinator
    (lambda (f)
        (lambda (lst) 
          (if (null? lst) 0
            (add1 (f (cdr lst)))))) )
  (list 1 2 3) )                            ; ==> 3

所以现在,在函数上调用Y等同于从中进行递归定义:

( y (lambda (f) (lambda (x) .... (f x) .... )) ) 
===  define f = (lambda (x) .... (f x) .... )

...但是使用letrec(或命名为 let)更好——更有效,在自引用环境框架中定义闭包。整个Y事物是对不可能实现的系统的理论练习——即在不可能命名事物的情况下,创建名称“指向”事物的绑定,指代事物。

顺便说一句,指向事物的能力是高等灵长类动物与动物界其他生物的区别 ⁄ 生物,至少我听说。:)

于 2012-08-08T12:47:11.220 回答
24

要查看会发生什么,请使用 DrRacket 中的步进器。步进器允许您查看所有中间步骤(并来回走动)。

将以下内容粘贴到 DrRacket:

(((lambda (mk-length)
    (mk-length mk-length))
  (lambda (mk-length)
    (lambda (l)
      (cond
        ((null? l) 0 )
        (else (add1
               ((mk-length mk-length)
                (cdr l))))))))
 '(a b c))

然后选择教学语言“Intermediate Student with lambda”。然后单击步进按钮(绿色三角形后跟一个条形)。

这是第一步的样子:

在此处输入图像描述

然后为第二个函数做一个例子,看看哪里出了问题。

于 2012-05-08T14:13:53.597 回答