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。
答案 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
。
那么,letrec
在代码库中有多少用途? 16.这与约let
和let*
的约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堆栈溢出上提出了一点点或多或少的相同点。