为什么 F# 和 OCaml(以及可能其他语言)中的函数默认情况下不是递归的?
换句话说,为什么语言设计者们会决定在声明中明确要求您键入 rec
,这被认为是一个好主意:
let rec foo ... = ...
为什么不默认给函数递归能力?为什么需要显式使用 rec
关键字?
原始ML的法国和英国后代做出了不同的选择,并且这些选择已经通过几十年传承到现代变体中。因此,这只是遗留问题,但确实影响了这些语言中的习语。
在法国CAML语言系列(包括OCaml)中,默认情况下函数不是递归的。这个选择使得在这些语言中使用let
轻松地替代函数(和变量)定义,因为您可以在新定义的主体内引用先前的定义。F#从OCaml继承了这种语法。
例如,在计算OCaml中序列的Shannon熵时替换函数p
:
let shannon fold p =
let p x = p x *. log(p x) /. log 2.0 in
let p t x = t +. p x in
-. fold p 0.0
shannon
的参数p
在函数体的第一行被另一个p
所取代,然后在函数体的第二行又被另一个p
所取代。fun
绑定函数默认是递归的。当大多数函数定义不需要访问其函数名的先前绑定时,这会导致代码更简单。但是,被替换的函数需要使用不同的名称(f1
,f2
等),这会污染作用域并使意外调用函数的错误“版本”成为可能。现在隐式递归的fun
绑定函数和非递归的val
绑定函数之间存在差异。let rec ... and ...
中,因为它会影响类型变量何时泛化。这与SML中默认情况下rec
有关,但与OCaml无关。rec
的显式使用在于 Hindley-Milner 类型推断,该推断是所有静态类型的函数式编程语言的基础(虽然以不同的方式改变和扩展)。如果您定义了 let f x = x
,则应该期望其具有类型 'a -> 'a
并且可在不同的 'a
类型上在不同的点上应用。但同样地,如果您编写 let g x = (x + 1) + ...
,则希望在 g
的其余部分中将 x
视为 int
。
Hindley-Milner 推断处理此区别的方式是通过显式的 泛化 步骤。在处理程序时,在特定的点上,类型系统停止并说“好的,这些定义的类型将在此处泛化,因此当有人使用它们时,它们类型中的任何自由类型变量都将被全新实例化,从而不会干扰此定义的任何其他用途。”这是个好主意的两个关键原因:
首先,如果启用递归定义,那么就不能引用同名值的先前绑定。当您正在扩展现有模块等操作时,这通常是一种有用的习惯用法。
其次,递归值,尤其是互相递归的值集合,比按顺序进行的定义(每个新定义都建立在已经定义的基础之上)更难理解。阅读这样的代码很好,除了显式标记为递归的定义外,新定义只能引用先前定义。
let
不仅用于绑定函数,还可以绑定其他常规值。大多数形式的值都不允许递归。某些递归形式的值是允许的(例如函数、惰性表达式等),因此需要明确的语法来指示这一点。let
结构类似于 Lisp 和 Scheme 中的 let
结构;它们都是非递归的。Scheme中有一个单独的letrec
结构用于递归的let。let rec xs = 0::ys and ys = 1::xs
。 - J D鉴于此:
let f x = ... and g y = ...;;
对比:
let f a = f (g a)
有了这个:
let rec f a = f (g a)
前者重新定义f
,将先前定义的f
应用于将g
应用于a
的结果。后者重新定义f
,无限循环地将g
应用于a
,这通常不是ML变体中想要的。
话虽如此,这是一种语言设计师的风格问题。就按照它去做吧。
let
、let*
和 let rec
的范围提供了不断增加的 power 和 cost。 let*
和 let rec
本质上是简单的 let
的嵌套版本,因此使用任何一个都更加昂贵。这种分级允许您微观管理程序的优化,因为您可以选择针对手头任务需要哪个 let 级别。如果您不需要递归或引用先前的绑定,则可以回退到简单的 let 来节省一些性能。
这类似于 Scheme 中分级相等谓词(即eq?
、eqv?
和 equal?
)。