好问题。为了那些没有正常运行 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 表达式内的那个点时减少它。在创建闭包之前减少它是荒谬的。一个微妙的错误。:)
当然,由于g
is 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.
不依赖于任何h
and 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事物是对不可能实现的系统的理论练习——即在不可能命名事物的情况下,创建名称“指向”事物的绑定,指代事物。
顺便说一句,指向事物的能力是高等灵长类动物与动物界其他生物的区别 ⁄ 生物,至少我听说。:)