【问题标题】:Example where letrec/letrec* is better than let with internal defines or named let ?letrec/letrec* 比带有内部定义或命名的 let 更好的示例?
【发布时间】:2012-01-29 14:13:42
【问题描述】:

Kent Dybvig 在 The Scheme Programming Language 中为 letrec 和 letrec* 提供的两个示例是:

(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 更简洁或更清晰。

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

【问题讨论】:

    标签: scheme


    【解决方案1】:

    是的,第一个示例可以使用名为 let 的方式重写,但请注意其中不需要 lambda 表单:

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

    这种转换有点误导——如果您定义一个“循环函数”(广义上的非尾递归)并立即在已知的输入。但是通常情况下,当您看到您给出的示例时,其目的是展示局部函数的定义和使用,因此可以进行这种转换,因为它只是一个用于演示的玩具示例。

    其次,请注意命名let 通常不是原始形式——几乎所有Scheme 实现都使用的实现策略是将该形式扩展为letrec。因此,如果您想了解 named-lets,了解 letrec 仍然是一个好主意。 (这是一个基本特性:能够通过递归作用域进行自引用。)

    最后,您给出的带有内部定义的示例类似于命名-lets:它是一个扩展为letrec 的语法糖(可以是正确的letrecletrec*使用 R5RS,并且在 R6RS 中必须是 letrec*)。所以为了理解它是如何工作的,你需要理解letrec。另请注意,某些使用严格的letrec 的实现也会在您的第二个示例中出错,并抱怨sum 未定义。正是这种语法糖化是 R6RS 中采用的letrec* 语义的主要论点的背后:许多人喜欢使用内部定义,但是存在一个问题,即顶层定义允许使用以前的定义,但内部定义在一种意想不到的方式。使用letrec*,内部定义就像顶级定义一样工作。 (更准确地说,它们的工作方式类似于顶级禁止重新定义,这意味着它们实际上类似于模块顶级定义。)

    (还要注意 (a) Racket 和 Chez 都扩展了内部主体以允许混合定义和表达式,这意味着扩展同样简单。)

    【讨论】:

    • Eli 你是不是很快 :-) 谢谢你的解释非常清楚。
    • 还有一个问题 Eli。你说命名的let不是原始形式,但通常实现为letrec。 letrec 通常是原始形式还是我们通常实现为带有 set! 的 let?类似地是让原始形式还是通常实现为 lambda。谢谢。
    • @HarrySpier: letrec 通常是原始的,尽管它经常做一些类似于letset!s 的事情。 (这几乎无关紧要,但可以使用延续来暴露。)至于let,我认为协议要少得多,有些实现使用lambda作为扩展,有些则将其视为原始。
    【解决方案2】:

    我赞同 Eli 的回答;命名为let 和内部define 是根据letrec 定义的。

    不过,我将为此添加一些经验性的、数字的轶事。我在一家使用 Scheme 的公司工作。我们有 981 个 Scheme 代码文件,总共有 206,878 行(计算 cmets、空白行等)。这是一个 8 到 16 人的团队在大约 8 年的时间里编写的。

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

    所以,我不会对 letrec 没有太多实际用途感到惊讶。

    【讨论】:

    • 这可能只是对您正在使用的精心设计的库的证明。您对letrec 的所有调用实际上都是在map 的幌子下进行的。
    【解决方案3】:

    从肯特的一些学生那里,我了解到:letrec 是根据let 使用宏扩展实现的。它将letrec 扩展为在内部使用set!let。因此,您的第一个示例将扩展为:

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

    你的第二个,类似地(注意嵌套的 lets 是 let* 的结果 - 而且,这可能不是一个完全正确的扩展,但这是我最好的猜测):

    (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))
    

    我不是 600% 确定命名的 let 是如何扩展的,但 Eli 建议它将根据 letrec 本身来实现(这是有道理的,应该很明显)。因此,您命名的 let 从命名的 let 移动到 letrec 到未命名的 let。而且你对第二个的重写看起来几乎与它的扩展完全一样。

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

    当然,如果你正在编译,它可能会在编译器中全部消失,所以只需使用你认为看起来更好的任何一个(我偏爱letrec,因为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 更多是内置的,因此不会进行宏扩展(就像let 一样)。最后,a similar issue 已经在不久前的 Lisp 堆栈溢出中引发了或多或少相同的点。

    【讨论】:

      猜你喜欢
      • 2011-04-13
      • 2011-01-04
      • 1970-01-01
      • 2013-05-07
      • 2020-07-24
      • 2018-11-10
      • 1970-01-01
      • 2021-03-23
      • 1970-01-01
      相关资源
      最近更新 更多