letrec / letrec *比内部定义或命名let更好的示例?

时间:2011-12-29 01:35:07

标签: scheme

Kent Dybvig在Letrec和letrec *的Scheme Programming Language中给出的两个例子是:

(letrec ([sum (lambda (x)
             (if (zero? x)
                 0
                 (+ x (sum (- x 1)))))])
   (sum 5))

(letrec* ([sum (lambda (x)
            (if (zero? x)
                    0
                (+ x (sum (- x 1)))))]
         [f (lambda () (cons n n-sum))]
         [n 15]
         [n-sum (sum n)])
  (f))

第一个也可以写为名为let:

(let sum ([x 5]) 
  ((lambda (x)
                 (if (zero? x)
                     0
                     (+ x (sum (- x 1))))) x))  

,第二个可以写成带有内部定义的let:

(let ()
  (define sum  (lambda (x)
               (if (zero? x)
               0 
                   (+ x (sum (- x 1))))))
  (define f (lambda () (cons n n-sum)))
  (define n 15)
  (define n-sum (sum n))
  (f))

letrec / letrec *表单似乎没有比使用内部定义表单的let let或let更简洁或更清晰。

有人可以给我看一个例子,其中letrec / letrec *确实改进了代码,或者是必要的,而不是使用内部定义命名let或let。

3 个答案:

答案 0 :(得分:6)

是的,可以使用命名的let重写第一个示例,但请注意,那里不需要lambda表单:

(let sum ([x 5])
  (if (zero? x)
    0
    (+ x (sum (- x 1)))))

这种转换有点误导 - 如果你定义一个单独的“循环函数”(在广义的非尾递归意义上)并且立即在已知的情况下使用它,这样做很好输入。但通常情况下,当你看到例如你给出的例子时,目的是显示本地函数的定义和使用,所以只有因为它是一个演示玩具的例子才能进行这种转换。

其次,请注意,名为let通常是一种原始形式 - 几乎所有Scheme实现都使用的实现策略是将该表单扩展为{{1} }。因此,如果您想了解名字 - letrec,理解letrec仍然是一个好主意。 (这是一个基本特征:能够通过递归范围进行自引用。)

最后,您使用内部定义提供的示例与命名 - let s类似:它是一个语法糖,扩展为let(可以是正确的letrec或者带有R5RS的letrec,并且在R6RS中必须是letrec*。因此,为了理解它是如何工作的,您需要了解letrec*。另请注意,使用严格letrec的某些实现也会在第二个示例中进行barf,并抱怨letrec未定义。在R6RS中采用的sum语义的主要论证背后是这种语法上的含糖:许多人喜欢使用内部定义,但是有一个问题,即顶层定义允许使用先前的定义,但内部定义较少以意想不到的方式方便。对于letrec*,内部定义的工作方式类似于顶层定义。 (更准确地说,它们的工作方式就是限制重新定义,这意味着它们实际上就像模块顶层定义。)

(另请注意:(a)Racket和Chez都扩展了内部实体,以允许混合定义和表达式,这意味着扩展非常简单。)

答案 1 :(得分:2)

我是第二个Eli的回答;名为let,内部define的定义是letrec

但是,我会在此添加一些经验的数字轶事。我在一家使用Scheme的公司工作。我们有981个Scheme代码文件,总计达206,878行(计算注释,空行,整个事物)。这是由一个团队撰写的,在大约8年的时间里,他们的人数在8-16人之间。

那么,letrec在代码库中有多少用途? 16.这与约letlet*的约7,000次使用相比较(估计因为我不打算改进我使用的正则表达式)。看起来所有的letrec用法都是由同一个人编写的。

所以,我不会感到惊讶的是你找不到letrec的实际用途。

答案 2 :(得分:1)

来自肯特的一些学生,我学到了以下内容:letrec是使用宏扩展实现的let。它将letrec扩展为内部使用let的{​​{1}}。所以你的第一个例子将扩展到:

set!

你的第二个,同样(注意嵌套的(let ([sum (void)]) (set! sum (lambda (x) (if (zero? x) 0 (+ x (sum (- x 1)))))) (sum 5)) let的结果 - 也可能不是一个完全正确的扩展,但这是我最好的猜测:

let*

我不是600%确定命名(let ([sum (void)] (set! sum (lambda (x) (if (zero? x) 0 (+ x (sum (- x 1)))))) (let [f (void)] (set! f (lambda () (cons n n-sum))) (let [n (void)] (set! n 15) (let [n-sum (void)]) (set! n-sum (sum n)) (f)) 如何扩展,但Eli建议它将以let本身的方式实现(这是有道理的,应该非常明显)。因此,您指定的letrec从已命名的let移动到let,转变为未命名的letrec。你重写的第二个看起来几乎就像它的扩展。

如果您正在解释它并寻找良好的表现,我会倾向于let因为它是一个较短的宏扩展步骤。此外,letrec变为lambda,因此您在第二个示例中使用let而不是define s(可能更重)。

当然,如果你正在编译,它可能会全部掉在编译器中,所以只要使用你认为看起来更好的那些(我偏向set!因为letrec循环提醒我命令式编程,但ymmv)。也就是说,它应该取决于你,风格(因为它们或多或少相当)。

那就是说,让我给你一个你可能觉得有价值的例子:

let

使用内部(letrec ([even? (lambda (n) (if (zero? n) #t (odd? (- n 1))))] [odd? (lambda (n) (if (zero? n) #f (even? (- n 1))))]) (even? 88)) 样式将产生:

define

所以这里的(let () (define even? (lambda (n) (if (zero? n) #t (odd? (- n 1))))) (define odd? (lambda (n) (if (zero? n) #f (even? (- n 1))))) (even? 88)) 代码实际上更短。老实说,如果你正在做像后者这样的事情,为什么不满足于letrec

begin

我怀疑(begin (define even? (lambda (n) (if (zero? n) #t (odd? (- n 1))))) (define odd? (lambda (n) (if (zero? n) #f (even? (- n 1))))) (even? 88)) 更多是内置的,因此不会进行宏扩展(例如begin会)。最后,a similar issue已经在Lisp堆栈溢出上提出了一点点或多或少的相同点。