为什么Ocaml / F#中的函数默认不递归?

时间:2009-05-23 00:59:14

标签: f# recursion ocaml

为什么F#中的函数和Ocaml(可能还有其他语言)默认不是递归的?

换句话说,为什么语言设计者决定明确地让你在如下声明中输入rec是个好主意:

let rec foo ... = ...

默认情况下不提供函数递归功能?为什么需要明确的rec构造?

6 个答案:

答案 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绑定函数是递归的。当大多数函数定义不需要访问其函数名的先前绑定时,这会产生更简单的代码。但是,取代函数会使用不同的名称(f1f2等),这会污染范围并使意外调用函数的错误“版本”成为可能。现在,隐式递归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) + ...,你希望xint的其余部分被视为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)

它的一个重要部分是它使程序员能够更好地控制本地范围的复杂性。 letlet*let rec的频谱提供了更高的功耗和成本。 let*let rec实际上是简单let的嵌套版本,因此使用其中任何一个都比较昂贵。此评分允许您对程序的优化进行微观管理,因为您可以选择手头任务所需的级别。如果您不需要递归或能够引用先前的绑定,那么您可以使用简单的let来节省一些性能。

它类似于Scheme中的分级等式谓词。 (即eq?eqv?equal?