letrec有啥好处?

Posted

技术标签:

【中文标题】letrec有啥好处?【英文标题】:What are the benefits of letrec?letrec有什么好处? 【发布时间】:2011-01-04 20:50:02 【问题描述】:

在阅读“经验丰富的计划者”时,我开始了解letrec。我理解它的作用(可以用 Y-Combinator 复制),但本书使用它来代替已经在保持静态的参数上运行的 defined 函数。

一个使用 defined 函数的旧函数的示例在自身上重复出现(没什么特别的):

(define (substitute new old l)
  (cond
    ((null? l) '())
    ((eq? (car l) old)
      (cons new (substitute new old (cdr l))))
    (else
      (cons (car l) (substitute new old (cdr l))))))

现在以使用 letrec 的相同函数为例:

(define (substitute new old l)
  (letrec
    ((replace
      (lambda (l)
        (cond
          ((null? l) '())
          ((eq? (car l) old)
           (cons new (replace (cdr l))))
          (else
           (cons (car l) (replace (cdr l))))))))
(replace lat)))

除了略长且更难阅读之外,我不知道他们为什么要重写书中的函数以使用 letrec。以这种方式重复静态变量时是否会提高速度,因为您不会一直传递它??

对于参数保持静态但一个参数减少的函数(例如向下循环列表的元素),这是标准做法吗?

来自更有经验的 Schemers/LISPers 的一些意见会有所帮助!

【问题讨论】:

【参考方案1】:

因此,您有一些涵盖可读性问题的答案,应该没问题。但一个不清楚的问题是是否存在任何性能问题。从表面上看,letrec 版本,如 named-let 版本(实际上是一样的)应该更快,因为在循环中传递的参数更少。然而,实际上编译器可以在你背后做各种优化,比如找出普通版本中的循环不改变地传递前两个参数,并将它变成一个带有第一个参数的单参数循环。这里不是向您显示特定系统上的数字,而是一个 PLT 模块,您可以运行它来计时四个不同的版本,并且您可以轻松添加更多以尝试其他变体。简短的总结是,在我的机器上,named-let 版本稍微快一些,并且使其尾递归具有更大的整体优势。

#lang scheme

;; original version
(define (substitute1 new old l)
  (cond [(null? l) '()]
        [(eq? (car l) old) (cons new (substitute1 new old (cdr l)))]
        [else (cons (car l) (substitute1 new old (cdr l)))]))

;; letrec version (implicitly through a named-let)
(define (substitute2 new old l)
  (let loop ([l l])
    (cond [(null? l) '()]
          [(eq? (car l) old) (cons new (loop (cdr l)))]
          [else (cons (car l) (loop (cdr l)))])))

;; making the code a little more compact
(define (substitute3 new old l)
  (let loop ([l l])
    (if (null? l)
      '()
      (cons (let ([fst (car l)]) (if (eq? fst old) new fst))
            (loop (cdr l))))))

;; a tail recursive version
(define (substitute4 new old l)
  (let loop ([l l] [r '()])
    (if (null? l)
      (reverse r)
      (loop (cdr l)
            (cons (let ([fst (car l)]) (if (eq? fst old) new fst)) r)))))

;; tests and timings

(define (rand-list n)
  (if (zero? n) '() (cons (random 10) (rand-list (sub1 n)))))

(for ([i (in-range 5)])
  (define l   (rand-list 10000000))
  (define new (random 10))
  (define old (random 10))
  (define-syntax-rule (run fun)
    (begin (printf "~a: " 'fun)
           (collect-garbage)
           (time (fun new old l))))
  ;; don't time the first one, since it allocates a new list to use later
  (define new-list (substitute1 new old l))
  (unless (and (equal? (run substitute1) new-list)
               (equal? (run substitute2) new-list)
               (equal? (run substitute3) new-list)
               (equal? (run substitute4) new-list))
    (error "poof"))
  (newline))

【讨论】:

像往常一样,写得很好,非常有帮助(感谢您提及计时模块)。我知道我得到了很多免费的帮助,所以,感谢 Eli 花时间发布你的答案。您与其他海报的评论讨论也有助于我不知道或没有偶然发现的小事。再次感谢!【参考方案2】:

一方面,letrec 版本允许您使用该函数,即使它的全局名称被重置为其他名称,例如

(define substitute
  ; stuff involving letrec
  )

(define sub substitute)
(set! substitute #f)

那么sub 仍然可以工作,而非letrec 版本则不能。

至于性能和可读性,后者可能是口味问题,而前者不应该有明显的不同(尽管我没有资格坚持这样,而且无论如何它也是依赖于实现的)。

另外,我个人实际上会使用名为let

(define (substitute new old lat) ; edit: fixed this line
  (let loop (
             ; whatever iteration variables are needed + initial values
            )
    ; whatever it is that substitute should do at each iteration
    ))

我发现这种方式更具可读性。 YMMV。

【讨论】:

+1 用于命名的 let。在这种情况下和类似情况下更有意义。虽然 letrec 允许您定义多个相互递归的函数。所以当你需要这样做时,你需要一个letrec。 set! 点在 PLT Scheme 中没有实际意义——一旦您在自己的模块中定义了一个函数,并且您永远不会在该模块中使用 set! 名称,其他任何人都无法更改它。这同样适用于所有具有 R6RS 或类似模块系统的方案——您所指的“技巧”也是旧的 R5RS-ism。 @Eli:是的,但是由于 OP 正在阅读“经验丰富的计划者”,因此现代计划的方法可能与他的经历无关。 :-) Michal:亲自了解作者,如果它有任何这样的 set! 技巧,我会感到非常惊讶......此外,如果/当他们被提及时,他们应该总是带有一个很大的免责声明说这是现在不应该相关的事情。 @Eli:要点...此外,请注意set! 现在通常不适用于***变量这一事实很重要,因此感谢您的评论。仍然,例如“编程语言方案”仍然包含set! 到顶层(参见当前网络文本中练习 3.5.1 稍上方的calc 示例),我认为习惯它们是值得的(以及相关的怪癖)只是为了阅读那本很酷的书。 :-)【参考方案3】:

关于您的具体示例:我个人认为letrec 版本更易于阅读:您定义了一个递归辅助函数,并在***函数的主体中调用它。这两种形式的主要区别在于,在letrec 形式中,您不必在递归调用中一遍又一遍地指定静态参数,我觉得这样更简洁。

如果代码被编译,避免在每个递归函数调用中传递静态参数也可能在这种情况下提供小的性能优势,因为调用者避免了将参数复制到新的堆栈帧中。如果递归函数调用位于尾部位置,编译器可能足够聪明,可以避免一遍又一遍地复制堆栈上的参数。

类似地,如果代码被解释,在递归调用中使用更少的参数会更快。

更一般地说,letrec 的主要好处之一是它允许相互递归的函数定义这一事实,我认为您在上面没有提到。我对方案非常缺乏经验,但据我所知,这是letrec 表单与例如相比的主要特征之一。 define.

【讨论】:

为了扩展这个答案——如果你挂在额外的 Veriage 上,它看起来不会立即更具可读性。为此,将循环表示为命名的 let 会更轻松。但让它更具可读性的一件事是,很明显循环只发生在一个变量上,这是一个好处(对于性能也是如此)。 @Eli:哦,所以它确实对性能有影响?知道这很有趣。命名的 let 在这方面会有所不同吗? 这是一个很好的答案,而且 cmets 也很好;你们的答案质量很好,我学到了很多东西。谢谢!

以上是关于letrec有啥好处?的主要内容,如果未能解决你的问题,请参考以下文章

冗余限定符有啥缺点吗?有啥好处吗?

MySQL中的zerofill有啥好处?

互联互通有啥好处?

使用 NSOutputstream 有啥好处?

使用静态方法有啥好处?

春季自动装配有啥好处