TL;DR:define
是 letrec
。这就是使我们能够首先编写递归定义的原因。
考虑
let fact = fun (n => (n==0 -> 1 ; n * fact (n-1)))
这个定义主体内的名称指的是什么实体fact
?With let foo = val
,val
是根据已知实体定义的,因此它不能引用foo
尚未定义的实体。就范围而言,可以说(并且通常是)let
等式的 RHS 是在外部范围内定义的。
fact
内部实际指向正在定义的实体的唯一方法是使用letrec
,其中允许定义的实体引用定义它的范围。因此,虽然在定义正在进行时对实体进行评估是一个错误,但存储对其(未来,在这个时间点)值的引用letrec
是可以的——在使用它的情况下。
你提到的define
,只是letrec
另一个名字。在计划中也是如此。
如果没有定义实体引用自身的能力,即在具有非递归的语言中let
,则必须使用诸如y-combinator之类的神秘设备。这很麻烦而且通常效率低下。另一种方式是定义如
let fact = (fun (f => f f)) (fun (r => n => (n==0 -> 1 ; n * r r (n-1))))
因此letrec
,为程序员带来了实现的效率和便利。
那么问题就变成了,为什么要公开非递归let
?Haskell 确实没有。Scheme 同时具有letrec
和let
。一个原因可能是为了完整性。另一个可能是一个更简单的实现let
,内存中的自引用运行时结构较少,这使得垃圾收集器更容易。
你要一个励志的例子。考虑将斐波那契数定义为自引用惰性列表:
letrec fibs = {0} + {1} + add fibs (tail fibs)
对于非递归,将定义let
列表的另一个副本,fibs
用作逐元素加法函数的输入add
。这将导致在其术语中定义另一个副本的定义。等等; 访问第n个斐波那契数将导致在运行时创建和维护n-1 个列表链!不是一张漂亮的照片。fibs
这是假设fibs
也使用了相同的方法tail fibs
。如果没有,所有赌注都取消。
需要的是fibs
使用它自己,引用它自己,所以只维护一个列表的副本。