首页 文章

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

提问于
浏览
5

Kent Dybvig在Letrec和letrec *的Scheme Scheme 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内部定义表单更简洁或更清晰 .

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

3 回答

  • 1

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

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

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

    其次,请注意,名为 let 的通常是 not 原始形式 - 实际上所有Scheme实现都使用的实现策略是将该形式扩展为 letrec . 因此,如果您想了解named- let ,那么理解 letrec 仍然是个好主意 . (这是一个基本特征:能够通过递归范围进行自引用 . )

    最后,你给出了内部定义的例子类似于named- let :它是一个语法糖,扩展为 letrec (可以是一个合适的 letrec 或一个带有R5RS的 letrec* ,并且在R6RS中必须是 letrec* ) . 所以为了理解它是如何工作的,你需要理解 letrec . 另请注意,使用strict letrec 的某些实现也会在第二个示例中进行barf,并抱怨 sum 未定义 . 在R6RS中采用的 letrec* 语义的主要论点背后是这种语法糖:许多人喜欢使用内部定义,但是有一个问题,即顶层定义允许使用先前的定义,但内部定义在意外时不太方便办法 . 对于 letrec* ,内部定义的工作方式类似于顶层定义 . (更准确地说,它们的工作方式就是限制重新定义,这意味着它们实际上就像模块顶层定义 . )

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

  • 2

    我是第二个Eli的答案;名为 let ,内部 defineletrec 定义 .

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

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

    所以,我'm not going to be surprised that you won'找到了很多实际用途 letrec .

  • 6

    从Kent的一些学生中,我学到了以下内容: letrec 使用宏扩展实现了 let . 它将 letrec 扩展为 let ,内部使用 set! . 所以你的第一个例子将扩展到:

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

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

    (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 从名为 letletrec 移动到未命名的 let . 你重写的第二个看起来几乎就像它的扩展 .

    如果你正在解释它并寻找良好的表现,我会倾向于 letrec ,因为它是一个缩短宏观扩张步骤 . 此外, let 变成了一个lambda,所以你在第二个例子中使用 define 而不是 set! (可能更重) .

    当然,如果你're compiling, it will probably all fall out in the compiler anyway so just use whichever you think looks nicer (I'偏偏于 letrec 因为 let 循环让我想起了命令式编程但是ymmv) . 也就是说,它应该取决于你,风格(因为它们或多或少相当) .

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

    (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堆栈溢出上提出了一点点或多或少相同的点 .

相关问题