Answers:
原始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函数是递归的。当大多数函数定义不需要访问其函数名称的先前绑定时,这将导致代码更简单。但是,被取代的函数将使用不同的名称(f1
,f2
等等),这会污染范围,并有可能意外地调用错误的函数“版本”。现在,隐式递归fun
绑定函数和非递归val
绑定函数之间存在差异。
Haskell可以通过将定义限制为纯净来推断定义之间的依赖关系。这使玩具样品看起来更简单,但在其他地方却要付出巨大的代价。
请注意,Ganesh和Eddie给出的答案是红色鲱鱼。他们解释了为什么不能将函数组放置在巨人内部,let rec ... and ...
因为它会影响何时泛化类型变量。这与rec
SML中的默认设置无关,而与OCaml 无关。
明确使用的一个关键原因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
但SML不需要的事实是一个明显的反例。如果由于您描述的原因而导致类型推断成为问题,则OCaml和SML不可能像他们那样选择其他解决方案。当然,原因是您正在谈论and
使Haskell具有相关性。
一些猜测:
let
不仅用于绑定函数,还用于绑定其他常规值。大多数形式的值均不允许递归。允许使用某些形式的递归值(例如函数,惰性表达式等),因此它需要显式语法来表明这一点。let
构造类似于let
Lisp and Scheme中的构造;这是非递归的。letrec
Scheme中有一个单独的构造用于递归让我们let rec xs = 0::ys and ys = 1::xs
。
它的很大一部分是它使程序员可以更好地控制其本地范围的复杂性。的频谱let
,let*
并let rec
提供电源和成本的增加水平。let*
并且let rec
本质上是simple的嵌套版本let
,因此使用其中任何一个都更昂贵。这种等级可以让您对程序的优化进行微观管理,因为您可以选择所需的任务级别。如果您不需要递归或引用以前的绑定的功能,则可以依靠简单的方法节省一些性能。
它类似于Scheme中的分级相等谓词。(即eq?
,eqv?
和equal?
)