关于递归:letrec作为编程语言特性的优点是什么

What are the merits of letrec as a programming language feature

我已经看过关于letrec的所有内容了,我仍然不明白它为一种语言带来了什么。 似乎所有用letrec表达的东西都可以很容易地写成递归函数。 但是,如果语言已经支持递归函数,是否有任何理由将letrec作为编程语言的一个特性公开? 为什么有几种语言同时暴露?

我得到letrec可能用于实现其他功能,包括递归函数,但这与它本身应该是一个功能的原因无关。 我还读到有些人发现它比某些lisps中的递归函数更具可读性,但同样这也不相关,因为该语言的设计者可以努力使递归函数足够可读,不需要其他功能。 最后,我被告知letrec可以更简洁地表达某些类型的递归值,但我还没有找到一个激励的例子。


TL; DR:defineletrec。这使我们能够首先编写递归定义。

考虑

1
let fact = fun (n => (n==0 -> 1 ; n * fact (n-1)))

在这个定义的正文中,名称fact指的是什么实体?对于let foo = valval是根据已知实体定义的,因此它不能引用尚未定义的foo。就范围而言,可以说(并且通常是)let方程的RHS在外部范围中定义。

内部fact实际指向正在定义的内部的唯一方法是使用letrec,其中允许定义的实体引用其定义的范围。因此,虽然在定义正在进行时导致实体的评估是一个错误,但是在使用letrec的情况下,存储对其(将来,在此时间点)值的引用是正常的。

您引用的define在另一个名称下只是letrec。在Scheme中也是如此。

如果没有定义实体的能力来引用自身,即在具有非递归let的语言中,为了具有递归,则必须求助于使用诸如y-组合器之类的神秘设备。这很麻烦,通常效率低下。另一种方式是定义

1
let fact = (fun (f => f f)) (fun (r => n => (n==0 -> 1 ; n * r r (n-1))))

因此letrec为表格带来了实现的效率和程序员的便利性。

然后问题成为,为什么要暴露非递归let?哈斯克尔确实没有。 Scheme具有letreclet。一个原因可能是完整性。另一个可能是let的更简单的实现,在内存中使用较少的自引用运行时结构使垃圾收集器更容易。

你要求一个动机的例子。考虑将Fibonacci数定义为自引用惰性列表:

1
letrec fibs = {0} + {1} + add fibs (tail fibs)

对于非递归let,将定义列表fibs的另一个副本,以用作元素加法函数add的输入。这将导致在其术语中定义另一个fibs副本的定义。等等;访问第n个Fibonacci数将导致在运行时创建并维护一系列n-1个列表!不是一张漂亮的照片。

并且假设tail fibs也用于tail fibs。如果不是,所有的赌注都会被取消。

需要的是fibs使用自身,引用自身,因此只保留列表的一个副本。


注意:虽然这不是Scheme特定的问题,但我正在使用Scheme来证明这些差异。希望你能读一些小的lisp代码

letrec只是一个特殊的let,其中绑定本身是在计算表示其值的表达式之前定义的。想象一下:

1
2
3
4
5
6
(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来救援:

1
2
3
4
5
6
(define (fib n)
  (letrec ((fib (lambda (n a b)
                  (if (zero? n)
                      a
                      (fib (- n 1) b (+ a b))))))
    (fib n))

那个letrec只是这样做的语法:

1
2
3
4
5
6
7
8
(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文章开始。