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


104

为什么默认情况下F#和Ocaml(以及其他语言)中的函数不是递归的?

换句话说,为什么语言设计人员认为最好让您键入如下rec声明是一个好主意:

let rec foo ... = ...

并默认不提供函数递归功能?为什么需要显式rec构造?


Answers:


87

原始ML的法国和英国后裔做出了不同的选择,并且他们的选择已继承了几十年的现代变体。因此,这只是遗留问题,但确实会影响这些语言中的习语。

默认情况下,法语CAML语言家族(包括OCaml)中的函数不是递归的。通过这种选择,可以轻松地取代使用let这些语言的函数(和变量)定义,因为您可以在新定义的主体内引用以前的定义。F#从OCaml继承了此语法。

例如,p在计算OCaml中的序列的Shannon熵时取代该函数:

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分支采用了另一种选择,fun默认情况下,SML的-bound函数是递归的。当大多数函数定义不需要访问其函数名称的先前绑定时,这将导致代码更简单。但是,被取代的函数将使用不同的名称(f1f2等等),这会污染范围,并有可能意外地调用错误的函数“版本”。现在,隐式递归fun绑定函数和非递归val绑定函数之间存在差异。

Haskell可以通过将定义限制为纯净来推断定义之间的依赖关系。这使玩具样品看起来更简单,但在其他地方却要付出巨大的代价。

请注意,Ganesh和Eddie给出的答案是红色鲱鱼。他们解释了为什么不能将函数组放置在巨人内部,let rec ... and ...因为它会影响何时泛化类型变量。这与recSML中的默认设置无关,而与OCaml 无关。


3
我不认为它们是一团糟:如果不是出于对推理的限制,则很可能像大多数其他语言一样,将整个程序或模块自动视为相互递归。那将决定是否需要“ rec”的特定设计决策。
GS-向Monica致歉,2009年

“ ...像大多数其他语言一样被自动视为相互递归”。BASIC,C,C ++,Clojure,Erlang,F#,Factor,Forth,Fortran,Groovy,OCaml,Pascal,Smalltalk和Standard ML则没有。
JD

3
C / C ++仅需要用于正向定义的原型,这实际上与显式标记递归无关。Java,C#和Perl当然具有隐式递归。关于“最多”的含义和每种语言的重要性,我们可能会进行无休止的辩论,因此让我们只谈谈“很多”其他语言。
GS-向Monica道歉(

3
“ C / C ++仅需要用于正向定义的原型,这实际上与显式标记递归无关”。仅在自递归的特殊情况下。在相互递归的一般情况下,在C和C ++中,前向声明都是必需的。
JD

2
实际上,C ++在类范围中不需要前向声明,即,静态方法可以在没有任何声明的情况下相互调用。
polkovnikov.ph

52

明确使用的一个关键原因rec是与Hindley-Milner类型推断有关,该推断是所有静态类型的函数式编程语言(尽管以各种方式更改和扩展)的基础。

如果您有一个定义let f x = x,则希望它具有类型,'a -> 'a并适用'a于不同点的不同类型。但同样,如果您撰写本文let g x = (x + 1) + ...,那么您x应该将其视为int的其余部分g

Hindley-Milner推理处理这种区别的方式是通过一个明确的概括步骤。在某些点处理程序时,类型系统停止,并说:“好了,这些定义的类型将在这一点上一概而论,所以,当有人使用它们,在它们的类型的任何自由的类型变量将新鲜实例化,并由此不会干扰此定义的任何其他用途。”

事实证明,进行这种概括的明智之举是在检查了一组相互递归的函数之后。任何更早的版本,您都会泛化得太多,从而导致类型实际上可能发生冲突的情况。任何以后的版本,您都会泛化得太少,做出的定义不能与多个类型实例化一起使用。

因此,鉴于类型检查器需要知道哪些定义集是相互递归的,它可以做什么?一种可能性是简单地对范围内的所有定义进行依赖项分析,然后将它们重新排列为最小的组。Haskell实际上是这样做的,但是在诸如F#(以及OCaml和SML)这样具有不受限制的副作用的语言中,这是一个坏主意,因为它也可能会重新排列副作用。因此,相反,它要求用户显式标记哪些定义是相互递归的,从而扩展了应该在何处进行泛化。


3
嗯不 您的第一段是错误的(您正在谈论“ and”而不是“ rec”的显式使用),因此,其余部分是不相关的。
JD

5
我从来不满意这个要求。感谢您的解释。Haskell设计出众的另一个原因。
Bent Rasmussen 2010年

9
没有!!!!这怎么会发生?这个答案是完全错误的!请阅读下面的Harrop答案或查看标准ML的定义(Milner,Tofte,Harper,MacQueen-1997年)[p.24]
lambdapower

9
正如我在答复中指出,该类型推断的问题是一个原因为需要拍摄,而并非是唯一的原因。乔恩的答案也是一个非常有效的答案(除了通常对Haskell的sn亵评论);我不认为两者是对立的。
GS-向Monica道歉(

16
“类型推断问题是需要rec的原因之一”。OCaml需要rec但SML不需要的事实是一个明显的反例。如果由于您描述的原因而导致类型推断成为问题,则OCaml和SML不可能像他们那样选择其他解决方案。当然,原因是您正在谈论and使Haskell具有相关性。
JD 2012年

10

这是一个好主意,主要有两个原因:

首先,如果启用递归定义,则不能引用先前具有相同名称的值的绑定。当您执行诸如扩展现有模块之类的操作时,这通常是一个有用的习惯。

第二,递归值,尤其是相互递归值集,很难按顺序进行定义,每个新定义都建立在已经定义的基础上。阅读此类代码时最好保证除明确标记为递归的定义外,新定义只能引用以前的定义。


4

一些猜测:

  • let不仅用于绑定函数,还用于绑定其他常规值。大多数形式的值均不允许递归。允许使用某些形式的递归值(例如函数,惰性表达式等),因此它需要显式语法来表明这一点。
  • 优化非递归函数可能会更容易
  • 在创建递归函数时创建的闭包需要包含指向函数本身的条目(以便函数可以递归调用自身),这使递归闭包比非递归闭包更为复杂。因此,当您不需要递归时,能够创建更简单的非递归闭包可能会很好。
  • 它允许您根据先前定义的函数或同名值来定义函数;虽然我认为这是不好的做法
  • 额外的安全性?确保您正在按预期进行。例如,如果您不希望它具有递归性,但是您不小心在函数内部使用了与函数本身相同的名称,则很可能会抱怨(除非之前已定义该名称)
  • let构造类似于letLisp and Scheme中的构造;这是非递归的。letrecScheme中有一个单独的构造用于递归让我们

“不允许将大多数形式的值进行递归。允许某些形式的递归值(例如函数,惰性表达式等),因此它需要显式语法来表明这一点。” 对于F#来说确实如此,但是我不确定您可以在哪里对OCaml做到这一点let rec xs = 0::ys and ys = 1::xs
JD

4

鉴于这种:

let f x = ... and g y = ...;;

比较:

let f a = f (g a)

有了这个:

let rec f a = f (g a)

前者重定义f以前定义的适用f于应用的结果ga。后者重新定义f到无限循环应用ga,这通常不是你想做的事,ML变种。

也就是说,这是语言设计师风格的事情。放手去做。


1

它的很大一部分是它使程序员可以更好地控制其本地范围的复杂性。的频谱letlet*let rec提供电源和成本的增加水平。let*并且let rec本质上是simple的嵌套版本let,因此使用其中任何一个都更昂贵。这种等级可以让您对程序的优化进行微观管理,因为您可以选择所需的任务级别。如果您不需要递归或引用以前的绑定的功能,则可以依靠简单的方法节省一些性能。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.