我看过有关letrec的所有内容,但我仍然不明白它为语言带来了什么。似乎所有用letrec表达的东西都可以很容易地写成递归函数。但是,如果语言已经支持递归函数,是否有任何理由将公开 letrec作为编程语言的一个特性?为什么有几种语言都会暴露出来?
我认为letrec可能用于实现其他功能,包括递归函数,但这与它本身应该是一个功能的原因无关。我还读到有些人发现它比某些lisps中的递归函数更具可读性,但同样这也不相关,因为该语言的设计者可以努力使递归函数足够可读,不需要其他功能。最后,我被告知letrec可以更简洁地表达某些类型的递归值,但我还没有找到一个激励的例子。
答案 0 :(得分:1)
TL; DR:define
letrec
。这使我们能够首先编写递归定义。
考虑
let fact = fun (n => (n==0 -> 1 ; n * fact (n-1)))
这个定义正文中的名称fact
指的是什么实体?对于let foo = val
,val
是根据已知实体定义的,因此它不能引用尚未定义的foo
。就范围而言,可以说(并且通常是)let
方程的RHS在外部范围内定义。
内部fact
实际指向正在定义的内容的唯一方法是使用letrec
,其中允许定义的实体引用定义它的范围。因此,虽然在定义正在进行时对实体进行评估是一个错误,但将引用存储到其(将来,此时的时间)值是正常的 - 在使用{{ 1}}就是。
您引用的letrec
只是define
另一个名称。在Scheme中也是如此。
如果没有定义实体的能力来引用自身,即在具有非递归letrec
的语言中,要进行递归,就必须求助于使用诸如y-combinator之类的神秘设备。这很麻烦,通常效率低下。另一种方式是像
let
因此let fact = (fun (f => f f)) (fun (r => n => (n==0 -> 1 ; n * r r (n-1))))
为实现效率和程序员的便利性提供了表格。
然后问题变成了,为什么要揭露非 - 递归letrec
?哈斯克尔确实没有。 Scheme包含let
和letrec
。一个原因可能是完整性。另一个可能是let
的更简单的实现,在内存中使用较少的自引用运行时结构使得垃圾收集器更容易。
你要求一个动机的例子。考虑将Fibonacci数定义为自引用惰性列表:
let
使用非递归letrec fibs = {0} + {1} + add fibs (tail fibs)
将定义列表let
的另一个副本,以用作元素添加函数fibs
的输入。这将导致的另一个副本的定义<{1}},以便在 条款中定义。add
。等等;访问 n 的Fibonacci数将导致在运行时创建和维护一系列 n-1 列表!不是一张漂亮的照片。
并且假设同样fibs
也用于fibs
。如果没有,所有的赌注都会被取消。
我需要的是tail fibs
使用本身,引用本身,因此只保留列表的一个副本。
答案 1 :(得分:0)
注意:虽然这不是Scheme特定的问题,但我使用Scheme来证明这些差异。希望你能阅读一些小的lisp代码
letrec
只是一个特殊的let
,其中绑定本身是在评估表示其值的表达式之前定义的。想象一下:
(define (fib n)
(let ((fib (lambda (n a b)
(if (zero? n)
a
(fib (- n 1) b (+ a b))))))
(fib n))
此代码失败,因为fib
的主体中确实存在let
,它确实存在于它定义的闭包中,因为在评估lambda时绑定不存在。为了解决这个问题letrec
来救援:
(define (fib n)
(letrec ((fib (lambda (n a b)
(if (zero? n)
a
(fib (- n 1) b (+ a b))))))
(fib n))
letrec
只是这样做的语法:
(define (fib n)
(let ((fib 'undefined))
(let ((tmp (lambda (n a b)
(if (zero? n)
a
(fib (- n 1) b (+ a b))))))
(set! fib tmp))
(fib n)))
所以在这里你清楚地看到fib
在lambda被评估时存在,并且绑定稍后被设置为闭包本身。绑定是相同的,只有它的指针已经改变。它的循环参考101 ..
那么当你创建一个全局函数时会发生什么?显然,如果要进行递归,则需要在评估lambda之前存在或者必须突变环境。它也需要解决同样的问题。
在函数式语言实现中,突变不正常,您可以使用Y(或Z)组合器解决此问题。
如果您对如何实施语言感兴趣,建议您从Matt Mights articles开始。