letrec作为编程语言功能的优点是什么?

时间:2017-12-10 13:58:54

标签: recursion functional-programming programming-languages letrec

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

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

2 个答案:

答案 0 :(得分:1)

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

考虑

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

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

内部fact实际指向正在定义的内容的唯一方法是使用letrec,其中允许定义的实体引用定义它的范围。因此,虽然在定义正在进行时对实体进行评估是一个错误,但将引用存储到其(将来,此时的时间)值是正常的 - 在使用{{ 1}}就是。

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

如果没有定义实体的能力来引用自身,即在具有非递归letrec的语言中,要进行递归,就必须求助于使用诸如之类的神秘设备。这很麻烦,通常效率低下。另一种方式是像

这样的定义
let

因此let fact = (fun (f => f f)) (fun (r => n => (n==0 -> 1 ; n * r r (n-1)))) 为实现效率和程序员的便利性提供了表格。

然后问题变成了,为什么要揭露 - 递归letrec?哈斯克尔确实没有。 Scheme包含letletrec。一个原因可能是完整性。另一个可能是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开始。