如何解决方案:`letrec`和`letcc`对效率至关重要吗?
我正在阅读Friedman和Felleisen的 The Seasoned Schemer ,但是我对他们的一些最佳实践感到有些不安。 作者特别建议:
- 使用
letrec
删除对于递归应用程序不变的参数; - 使用
letrec
隐藏和保护功能; - 使用
letcc
迅速而迅速地返回值。
让我们研究一下这些规则的一些后果。 例如,考虑以下代码来计算列表列表的交集:
#lang scheme
(define intersectall
(lambda (lset)
(let/cc hop
(letrec
[[A (lambda (lset)
(cond [(null? (car lset)) (hop '())]
[(null? (cdr lset)) (car lset)]
[else (I (car lset) (A (cdr lset)))]))]
[I (lambda (s1 s2)
(letrec
[[J (lambda (s1)
(cond [(null? s1) '()]
[(M? (car s1) s2) (cons (car s1) (J (cdr s1)))]
[else (J (cdr s1))]))]
[M? (lambda (el s)
(letrec
[[N? (lambda (s)
(cond [(null? s) #f]
[else (or (eq? (car s) el) (N? (cdr s)))]))]]
(N? s)))]]
(cond [(null? s2) (hop '())]
[else (J s1)])))]]
(cond [(null? lset) '()]
[else (A lset)])))))
此示例出现在第13章中(不完全像这样:我粘贴了上一段中分别定义的成员资格测试代码)。
我认为以下letrec
和letcc
的使用非常有限的替代实现更具可读性和易懂性:
(define intersectall-naive
(lambda (lset)
(letrec
[[IA (lambda (lset)
(cond [(null? (car lset)) '()]
[(null? (cdr lset)) (car lset)]
[else (intersect (car lset) (IA (cdr lset)))]))]
[intersect (lambda (s1 s2)
(cond [(null? s1) '()]
[(M? (car s1) s2) (cons (car s1) (intersect (cdr s1) s2))]
[else (intersect (cdr s1) s2)]))]
[M? (lambda (el s)
(cond [(null? s) #f]
[else (or (eq? (car s) el) (M? el (cdr s)))]))]]
(cond [(null? lset) '()]
[else (IA lset)]))))
我是该计划的新手,但我的背景不是计算机科学,但令我惊讶的是,对于一个简单的列表交集问题,我们不得不以如此复杂的代码结束。这让我想知道人们如何管理现实应用程序的复杂性。
有经验的策划者是否会花费大量时间嵌套letcc
和letrec
表达式?
这是询问stackexchange的动机。
我的问题是:为了教育起见,Friedman和Felleisen是否使该示例过于复杂?还是出于性能原因,我是否应该习惯于编写包含letcc
和letrec
的代码?
对于大型列表,我的幼稚代码会变得不切实际吗?
解决方法
我不是Scheme实现的专家,但是我对这里发生的事情有一些想法。作者通过他们的let/cc
所没有的一个优点是,当清楚整个结果将是什么时,就可以提早终止。假设有人评估
(intersectall-naive (list big-list
huge-list
enormous-list
gigantic-list
'()))
您的IA
会将其转换为
(intersect big-list
(intersect huge-list
(intersect enormous-list
(intersect gigantic-list
'()))))
这是足够合理的。将首先计算最里面的交集,由于gigantic-list
不为零,因此它将遍历整个gigantic-list
,对每个项目检查该项目是否为'()
的成员。当然,没有结果,因此结果为'()
,但是您确实必须遍历整个输入才能找到答案。此过程将在每个嵌套的intersect
调用中重复:您的内部过程无法发出“绝望,只是放弃”的信号,因为它们仅通过返回值进行通信。
当然,您可以在没有let/cc
的情况下解决此问题,方法是在继续之前检查每个intersect
调用的返回值是否为空。但是(a)仅在一个方向而不是在两个方向上进行检查是相当不错的,并且(b)并非所有问题都如此令人满意:也许您想返回无法轻易发出信号以至于提早退出的东西是理想的。 let/cc
方法是通用的,可以在任何情况下提早退出。
关于使用letrec
来避免对递归调用重复使用常量参数:再次,我不是Scheme实现的专家,但是在Haskell中,我听到了这样的指导:如果仅关闭1个参数,它就是清洗,并为2个以上的参数提高性能。考虑到闭包的存储方式,这对我来说很有意义。但是我怀疑它在任何意义上都是“关键的”,除非您有大量的参数或递归函数所做的工作很少:参数处理将仅占完成的工作的一小部分。我发现作者认为这样做可以提高清晰度,而不是出于性能原因而这样做,我不会感到惊讶。如果我看到
(define (f a x y z)
(define (g n p q r) ...)
(g (g (g (g a x y z) x y z) x y z) x y z))
我会比看到的不那么开心
(define (f a x y z)
(define (g n) ...)
(g (g (g (g a)))))
因为我必须发现实际上p
只是x
等的另一个名称,所以请检查相同的x
,y
和{{1} },并确认这是故意的。在后一种情况下,很明显z
始终具有该含义,因为没有其他变量保存该值。当然,这是一个简化的示例,无论如何x
的四个文字应用我都不会感到兴奋,但是对于递归函数,同样的想法仍然适用。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。