为什么默认情况下 F# 和 OCaml(可能还有其他语言)中的函数不是递归的?
换句话说,为什么语言设计者认为明确地让你输入rec
如下声明是个好主意:
let rec foo ... = ...
并且默认不赋予函数递归能力?为什么需要显式rec
构造?
最初的 ML 的法国和英国后裔做出了不同的选择,他们的选择几十年来一直被继承到现代变体中。所以这只是遗留问题,但它确实会影响这些语言的习语。
在法语 CAML 系列语言(包括 OCaml)中,函数默认不是递归的。这种选择使取代let
在这些语言中使用的函数(和变量)定义变得容易,因为您可以在新定义的主体内引用先前的定义。F# 从 OCaml 继承了这种语法。
例如,p
在 OCaml 中计算序列的香农熵时取代函数:
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
注意p
高阶shannon
函数的参数是如何p
被主体的第一行中的另一个替代的,然后是主体p
第二行中的另一个。
相反,ML 语言家族的英国 SML 分支采取了另一种选择,并且 SML 的fun
-bound 函数默认情况下是递归的。当大多数函数定义不需要访问其函数名称的先前绑定时,这会导致代码更简单。但是,被取代的函数使用不同的名称(f1
等f2
),这会污染范围并可能意外调用错误的函数“版本”。现在隐式递归fun
绑定函数和非递归val
绑定函数之间存在差异。
Haskell 可以通过将定义限制为纯定义来推断定义之间的依赖关系。这使玩具样品看起来更简单,但在其他地方却付出了沉重的代价。
请注意,Ganesh 和 Eddie 给出的答案是红鲱鱼。他们解释了为什么不能将函数组放在一个巨大的内部,let rec ... and ...
因为它会影响类型变量何时被泛化。这与 SML 中的默认值无关,但与rec
OCaml 中的默认值无关。
显式使用的一个关键原因rec
是与 Hindley-Milner 类型推断有关,它是所有静态类型函数式编程语言的基础(尽管以各种方式进行了更改和扩展)。
如果你有一个定义let f x = x
,你会期望它有类型'a -> 'a
并且适用于'a
不同点的不同类型。但同样,如果你写let g x = (x + 1) + ...
,你会期望x
在.int
g
Hindley-Milner 推理处理这种区别的方式是通过明确的概括步骤。在处理程序的某些时刻,类型系统停止并说“好的,此时这些定义的类型将被泛化,因此当有人使用它们时,其类型中的任何自由类型变量都将被重新实例化,因此不会干扰该定义的任何其他用途。”
事实证明,进行这种概括的明智之处是在检查一组相互递归的函数之后。任何更早的,你都会概括太多,导致类型实际上可能发生冲突的情况。稍后,您将泛化得太少,从而使定义不能用于多种类型的实例化。
那么,鉴于类型检查器需要知道哪些定义集是相互递归的,它可以做什么呢?一种可能性是简单地对范围内的所有定义进行依赖性分析,并将它们重新排序为尽可能小的组。Haskell 实际上是这样做的,但是在像 F#(以及 OCaml 和 SML)这样具有不受限制的副作用的语言中,这是一个坏主意,因为它也可能对副作用进行重新排序。因此,它要求用户明确标记哪些定义是相互递归的,从而扩展应该在哪里进行泛化。
这是一个好主意有两个关键原因:
首先,如果您启用递归定义,那么您不能引用以前绑定的同名值。当你在做一些像扩展现有模块这样的事情时,这通常是一个有用的习惯用法。
其次,递归值,尤其是相互递归值的集合,更难推理,因为定义是按顺序进行的,每个新定义都建立在已经定义的之上。阅读这样的代码时最好能保证,除了明确标记为递归的定义外,新定义只能引用以前的定义。
一些猜测:
let
不仅用于绑定函数,还用于绑定其他常规值。大多数形式的值不允许递归。某些形式的递归值是允许的(例如函数、惰性表达式等),因此需要明确的语法来表明这一点。let
构造类似于let
Lisp 和 Scheme 中的构造;这是非递归的。Scheme中有一个单独的letrec
构造用于递归让我们鉴于这种:
let f x = ... and g y = ...;;
比较:
let f a = f (g a)
有了这个:
let rec f a = f (g a)
前者重新定义f
以将先前定义的应用到应用到f
的结果中。后者重新定义为循环永远应用于,这通常不是您在 ML 变体中想要的。g
a
f
g
a
也就是说,这是语言设计师风格的事情。放手去做。
其中很大一部分是它使程序员可以更好地控制其本地范围的复杂性。的频谱let
,let*
并let rec
提供越来越高的功率和成本水平。let*
并且let rec
本质上是 simple 的嵌套版本let
,因此使用任何一个都更昂贵。此分级允许您对程序的优化进行微观管理,因为您可以选择手头任务所需的级别。如果您不需要递归或引用先前绑定的能力,那么您可以依靠一个简单的 let 来节省一些性能。
它类似于 Scheme 中的分级相等谓词。(即eq?
和)eqv?
equal?