为什么F#中的函数和Ocaml(可能还有其他语言)默认不是递归的?
换句话说,为什么语言设计者决定明确地让你在如下声明中输入rec
是个好主意:
let rec foo ... = ...
默认情况下不提供函数递归功能?为什么需要明确的rec
构造?
答案 0 :(得分:80)
最初的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
注意高阶p
函数的参数shannon
如何被身体第一行中的另一个p
取代,然后第二行中的另一个p
取代身体的一条线。
相反,ML系列语言的英国SML分支采用了另一种选择,默认情况下SML的fun
绑定函数是递归的。当大多数函数定义不需要访问其函数名的先前绑定时,这会产生更简单的代码。但是,取代函数会使用不同的名称(f1
,f2
等),这会污染范围并使意外调用函数的错误“版本”成为可能。现在,隐式递归fun
- 绑定函数和非递归val
绑定函数之间存在差异。
Haskell可以通过将定义限制为纯粹来推断定义之间的依赖关系。这使得玩具样本看起来更简单,但其他地方的成本却很高。
请注意,Ganesh和Eddie给出的答案是红色鲱鱼。他们解释了为什么函数组不能放在巨型let rec ... and ...
中,因为它会影响类型变量的推广。这与rec
在SML中默认但与OCaml无关。
答案 1 :(得分:52)
显式使用rec
的一个重要原因是与Hindley-Milner类型推断有关,它是所有静态类型函数式编程语言的基础(尽管以各种方式进行了更改和扩展)。
如果您有一个定义let f x = x
,则您希望它具有'a -> 'a
类型,并适用于不同点的不同'a
类型。但同样,如果你写let g x = (x + 1) + ...
,你希望x
在int
的其余部分被视为g
。
Hindley-Milner推理处理这种区别的方式是通过明确的泛化步骤。在处理程序的某些时刻,类型系统停止并说“好了,这些定义的类型将在此时推广,这样当有人使用它们时,其类型中的任何自由类型变量将新< / em>实例化,因此不会干扰此定义的任何其他用途。“
事实证明,进行这种泛化的合理位置是在检查相互递归的函数集之后。任何更早的,你会概括太多,导致类型实际上可能发生碰撞的情况。以后任何一个,并且你会概括太少,使得定义不能用于多种类型的实例化。
因此,鉴于类型检查器需要知道哪些定义集是相互递归的,它能做什么?一种可能性是简单地对范围中的所有定义进行依赖性分析,并将它们重新排序到最小的可能组中。 Haskell实际上是这样做的,但是在F#(和OCaml和SML)等具有无限制副作用的语言中,这是一个坏主意,因为它可能会重新排序副作用。因此,它要求用户明确标记哪些定义是相互递归的,因此通过扩展来进行泛化。
答案 2 :(得分:10)
这是一个好主意有两个关键原因:
首先,如果启用递归定义,则无法引用先前对同名值的绑定。当您执行扩展现有模块之类的操作时,这通常是一个有用的习惯用法。
其次,递归值,尤其是相互递归值的集合,更难以推理,然后定义按顺序进行,每个新定义都建立在已定义的基础之上。阅读此类代码以保证除了明确标记为递归的定义之外,新定义只能引用先前的定义。
答案 3 :(得分:4)
一些猜测:
let
不仅用于绑定函数,还用于其他常规值。大多数形式的值都不允许递归。允许某些形式的递归值(例如函数,惰性表达式等),因此它需要一个显式语法来指示这一点。let
构造类似于Lisp和Scheme中的let
构造;这是非递归的。 Scheme for recursive let's letrec
构造
答案 4 :(得分:3)
鉴于此:
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变种。
那就是说,这是一种语言设计师风格的东西。随便一起去。
答案 5 :(得分:1)
它的一个重要部分是它使程序员能够更好地控制本地范围的复杂性。 let
,let*
和let rec
的频谱提供了更高的功耗和成本。 let*
和let rec
实际上是简单let
的嵌套版本,因此使用其中任何一个都比较昂贵。此评分允许您对程序的优化进行微观管理,因为您可以选择手头任务所需的级别。如果您不需要递归或能够引用先前的绑定,那么您可以使用简单的let来节省一些性能。
它类似于Scheme中的分级等式谓词。 (即eq?
,eqv?
和equal?
)