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

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

换句话说,为什么语言devise者决定明确地让你在一个声明中inputrec是个好主意:

 let rec foo ... = ... 

而不是默认给予函数recursion能力? 为什么需要一个明确的rec结构?

原ML的法国和英国后裔作出了不同的select,他们的select已经延续到现代的变种。 所以这只是遗产,但它确实影响这些语言的成语。

在法语CAML系列语言(包括OCaml)中,函数默认不recursion。 这个select可以很容易地使用let在这些语言中取代函数(和variables)定义,因为您可以引用新定义体内的先前定义。 F#从OCamlinheritance了这个语法。

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

 let shannon fold p = let px = px *. log(px) /. log 2.0 in let ptx = t +. px in -. fold p 0.0 

注意高阶shannon函数的参数p是如何被身体的第一行中的另一个p和身体的第二行中的另一个p取代的。

相反,ML系列语言的英国SML分支采取了其他select,SML的fun函数默认是recursion的。 当大多数函数定义不需要访问以前的函数名称绑定时,这会导致更简单的代码。 但是,被替代的函数会使用不同的名称( f1f2等)来污染范围,并且可能会意外地调用错误的“版本”函数。 现在隐式recursion的fun -bound函数和non-recursive val -bound函数之间存在差异。

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

请注意,Ganesh和Eddie给出的答案是红鲱鱼。 他们解释了为什么一组函数不能放在一个巨大的let rec ... and ...let rec ... and ...因为它会影响typesvariables的泛化。 这与rec默认为SML而不是OCaml无关。

明确使用rec一个重要原因是Hindley-Milnertypes推理,它是所有静态types函数式编程语言的基础(尽pipe以各种方式进行了改变和扩展)。

如果你有一个定义let fx = x ,你会期望它具有types'a -> 'a并适用于不同的'a在不同的点types。 但同样,如果你写出let gx = (x + 1) + ... ,你会期望xg的其他部分被当作int来处理。

Hindley-Milner推断处理这种区分的方式是通过明确的概括步骤。 在处理你的程序的某些时候,types系统停止并且说“好的,这些定义的types将在这个时候被泛化,以便当有人使用它们时,它们types中的任何自由typesvariables将被新鲜地实例化,不会干涉这个定义的任何其他用途。“

事实certificate,做这个概括的明智的地方是在检查一个相互recursion的函数之后。 任何更早,你会泛化太多,导致types可能实际上碰撞的情况。 任何后来,你会泛化得太less,使定义,不能用于多个types的实例。

因此,鉴于types检查器需要知道哪些定义是相互recursion的,它可以做什么? 一种可能性是简单地对范围内的所有定义进行依赖性分析,并将其重新排列成尽可能最小的组。 Haskell实际上是这样做的,但是在像F#(和OCaml和SML)这样的语言中,这些语言有不受限制的副作用,这是一个坏主意,因为它可能会重新sorting副作用。 因此,它要求用户明确地标记哪些定义是相互recursion的,从而扩展到泛化应该发生的地方。

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

首先,如果启用recursion定义,则不能引用以前同名的值的绑定。 当你正在做一些扩展现有模块的工作时,这通常是一个有用的习惯用法。

其次,recursion值,特别是相互recursion值的集合更难推理,然后按顺序进行定义,每个新的定义build立在已经定义的基础之上。 阅读这样的代码以保证除了明确标记为recursion的定义外,新定义只能引用先前的定义。

有些猜测:

  • let不仅用于绑定函数,还用于其他常规值。 大多数forms的值不允许recursion。 某些forms的recursion值是允许的(例如函数,惰性expression式等),所以它需要一个明确的语法来表示这个。
  • 优化非recursion函数可能会更容易
  • 在创buildrecursion函数时创build的闭包需要包含一个指向函数本身的入口(所以函数可以recursion地调用它自己),这使得recursion闭包比非recursion闭包更加复杂。 所以,当你不需要recursion时,能够创build更简单的非recursion闭包可能是件好事
  • 它允许您根据先前定义的函数或同名的值来定义函数; 虽然我认为这是不好的做法
  • 额外的安全? 确保你正在做你想要的。 例如:如果你不打算recursion,但是你不小心在函数内部使用了一个和函数本身同名的名字,它很可能会抱怨(除非这个名字已经被定义过了)
  • let构造与Lisp和Scheme中的let构造相似; 这是非recursion的。 Scheme中有一个单独的letrec结构,用于recursion的let

鉴于这种:

 let fx = ... and gy = ...;; 

比较:

 let fa = f (ga) 

有了这个:

 let rec fa = f (ga) 

前者重新定义f将以前定义的f应用到a的结果。 后者重新定义f循环永远应用ga ,这通常不是你想要的ML变种。

这就是说,这是一个语言devise师风格的东西。 放手去做。

其中很大一部分是它使程序员能更好地控制局部范围的复杂性。 let rec let* let rec提供了一个越来越高的功率和成本水平。 let*let rec在本质上嵌套版本的简单let ,所以使用任何一个更昂贵。 这个评分可以让你对程序的优化进行微观pipe理,因为你可以select哪种级别的任务来完成手头的任务。 如果你不需要recursion或者引用上一个绑定的能力,那么你可以简单地回退以节省一些性能。

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