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

nsa*_*llo 91 recursion f# ocaml

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

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

let rec foo ... = ...
Run Code Online (Sandbox Code Playgroud)

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

Jon*_*rop 83

最初的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
Run Code Online (Sandbox Code Playgroud)

注意p高阶shannon函数的参数如何在p正文的第一行中被另一个函数取代,然后p在正文的第二行中被另一个函数取代.

相反,ML系列语言的英国SML分支采用了另一种选择,fun默认情况下SML的绑定函数是递归的.当大多数函数定义不需要访问其函数名的先前绑定时,这会产生更简单的代码.然而,所取代的功能是由使用不同的名称(f1,f2等),污染范围并有可能意外地调用函数的错"版本".现在隐式递归fun绑定函数和非递归val绑定函数之间存在差异.

Haskell可以通过将定义限制为纯粹来推断定义之间的依赖关系.这使得玩具样本看起来更简单,但其他地方的成本却很高.

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

  • 我不认为它们是红色的:如果它不是对推理的限制,那么整个程序或模块可能会像大多数其他语言一样被自动视为相互递归.这将使具体的设计决定是否应该要求"rec". (3认同)
  • C/C++只需要原型用于前向定义,这实际上并不是明确标记递归.Java,C#和Perl肯定有隐式递归.我们可以就"最"的含义和每种语言的重要性进行无休止的辩论,所以让我们只选择"很多"其他语言. (3认同)
  • "C/C++只需要原型用于前向定义,这实际上并不是明确地标记递归".只有在自我递归的特殊情况下.在相互递归的一般情况下,前向声明在C和C++中都是必需的. (3认同)
  • 实际上,C++ 中的类作用域不需要前向声明,即静态方法无需任何声明即可相互调用。 (3认同)

GS *_*ica 51

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

如果您有一个定义let f x = x,那么您希望它具有类型'a -> 'a并适用于不同'a类型的不同类型.但同样地,如果你写作let g x = (x + 1) + ...,你会期望x被视为int在其余部分g.

Hindley-Milner推理处理这种区别的方式是通过明确的泛化步骤.在处理程序时的某些时刻,类型系统会停止并说"好了,这些定义的类型将在此时推广,这样当有人使用它们时,其类型中的任何自由类型变量都将被实例化,因此不会干扰这个定义的任何其他用途."

事实证明,进行这种泛化的合理位置是在检查相互递归的函数集之后.任何更早的,你会概括太多,导致类型实际上可能发生碰撞的情况.任何以后,你会概括太少,使得定义不能用于多种类型的实例化.

因此,鉴于类型检查器需要知道哪些定义集是相互递归的,它能做什么?一种可能性是简单地对范围中的所有定义进行依赖性分析,并将它们重新排序到最小的可能组中.Haskell实际上是这样做的,但是在F#(和OCaml和SML)等具有无限制副作用的语言中,这是一个坏主意,因为它可能会重新排序副作用.因此,它要求用户明确标记哪些定义是相互递归的,因此通过扩展来进行泛化.

  • "类型推断问题是需要rec的原因之一".事实上,OCaml需要`rec`但SML不是一个明显的反例.如果由于您描述的原因导致类型推断是问题,则OCaml和SML不能像他们那样选择不同的解决方案.当然,原因是你正在谈论`和`以使Haskell相关. (16认同)
  • 没有!!!!这怎么可能发生?!这个答案是完全错的!请阅读下面的Harrop的答案或查看_标准ML的定义(Milner,Tofte,Harper,MacQueen - 1997)[p.24] (9认同)
  • 正如我在回答中所说,类型推断问题是需要rec的原因之一*,而不是唯一的原因.Jon的答案也是一个非常有效的答案(除了关于Haskell的通常的讽刺评论); 我不认为这两者是对立的. (9认同)
  • 我对此要求从未满意.感谢您的解释.Haskell设计优越的另一个原因. (4认同)
  • 呃,不.你的第一段是错的(你在谈论明确使用"和"而不是"rec"),因此,其余部分是无关紧要的. (3认同)

zrr*_*zrr 10

这是一个好主意有两个关键原因:

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

其次,递归值,尤其是相互递归值的集合,更难以推理,然后定义按顺序进行,每个新定义都建立在已经定义的基础之上.在阅读此类代码时,可以保证除了明确标记为递归的定义之外,新定义只能引用先前的定义.


new*_*cct 5

一些猜测:

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