回答:
元のMLのフランスとイギリスの子孫は異なる選択を行い、それらの選択は数十年にわたって現代のバリアントに継承されました。したがって、これは単なるレガシーですが、これらの言語のイディオムに影響します。
フランス語のCAML言語ファミリ(OCamlを含む)では、デフォルトで関数は再帰的ではありません。この選択によりlet
、新しい定義の本体内で以前の定義を参照できるため、これらの言語で使用している関数(および変数)の定義を簡単に置き換えることができます。F#はこの構文をOCamlから継承しました。
たとえばp
、OCamlでシーケンスのシャノンエントロピーを計算するときに、関数を置き換えます。
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
の2行目で別の引数に置き換えられていることに注意してください。
逆に、言語のMLファミリーの英国のSMLブランチは他の選択を採用し、SMLのfun
バインドされた関数はデフォルトで再帰的です。ほとんどの関数定義が関数名の以前のバインディングにアクセスする必要がない場合、これによりコードが単純になります。しかし、代わら機能は、異なる名前(使用するように作られていますf1
、f2
など)のスコープを汚染し、誤っ機能の間違った「バージョン」呼び出しすることが可能になるました。また、暗黙的に再帰的にfun
バインドされた関数と非再帰的にバインドされた関数との間に不一致がありval
ます。
Haskellは、定義を純粋に制限することにより、定義間の依存関係を推測することを可能にします。これにより、おもちゃのサンプルはシンプルに見えますが、他の場所では重大なコストがかかります。
ガネーシュとエディによって与えられた答えは赤いニシンであることに注意してください。彼らはlet rec ... and ...
、型変数がいつ一般化されるかに影響を与えるため、関数のグループを巨人の中に配置できない理由を説明しました。これはrec
、SMLのデフォルトであることとは関係ありませんが、OCaml とは関係ありません。
を明示的に使用する重要な理由の1つrec
は、すべての静的に型付けされた関数型プログラミング言語の基礎となるHindley-Milner型推論を行うことです(さまざまな方法で変更および拡張されます)。
あなたが定義を持っている場合let f x = x
、あなたはそれが型を持つことを期待したい'a -> 'a
と異なるの適用可能と'a
異なる点でタイプ。しかし、同様に、あなたがを書いた場合let g x = (x + 1) + ...
、あなたはの体の残りの部分x
として扱われることを期待するでしょう。int
g
Hindley-Milner推論がこの区別を処理する方法は、明示的な一般化ステップによるものです。プログラムを処理する特定の時点で、型システムは停止し、「OK、これらの定義の型はこの時点で一般化されます。そのため、誰かがそれらを使用すると、その型の任意の自由型変数が新たにインスタンス化されるため、この定義の他の使用を妨げることはありません。」
この一般化を行うための賢明な場所は、相互に再帰的な関数のセットをチェックした後です。以前は、一般化しすぎて、型が実際に衝突する可能性がある状況につながります。後で、一般化しすぎて、複数の型のインスタンス化で使用できない定義を作成します。
では、型チェッカーが相互に再帰的な定義のセットについて知る必要がある場合、何ができるでしょうか?1つの可能性は、スコープ内のすべての定義に対して依存関係分析を行い、それらを可能な限り最小のグループに並べ替えることです。Haskellは実際にこれを行いますが、F#(およびOCamlやSML)などの制限のない副作用がある言語では、副作用も並べ替えられる可能性があるため、これは悪い考えです。したがって、代わりに、どの定義が相互に再帰的であるかを明示的にマークするようにユーザーに要求します。したがって、一般化が発生する必要がある拡張によって。
rec
がSMLは必要としないという事実は、明白な反例です。あなたが説明する理由のために型推論が問題であるならば、OCamlとSMLは彼らがしたように異なるソリューションを選択できなかったでしょう。もちろんその理由は、and
Haskellを適切にするためにあなたが話しているからです。
いくつかの推測:
let
関数のバインドだけでなく、他の通常の値にも使用されます。ほとんどの形式の値は、再帰的であってはなりません。特定の形式の再帰的な値(たとえば、関数、遅延式など)が許可されているため、これを示す明示的な構文が必要です。let
構成はlet
、LispおよびSchemeの構成に似ています。非再帰的です。別にありますletrec
再帰的なLet'sのSchemeに構成要素let rec xs = 0::ys and ys = 1::xs
。
これを考えると:
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バリアントでは必要ありません。
とはいえ、それは言語デザイナーのようなものです。そのままにしてください。
その大きな部分は、プログラマがローカルスコープの複雑さをより細かく制御できることです。スペクトルlet
、let*
及びlet rec
電力及びコストの両方の増加レベルを提供します。let*
そしてlet rec
本質的にシンプルな入れ子バージョンですlet
いずれかを使用すると、より高価であるので、。このグレーディングを使用すると、現在のタスクに必要なletのレベルを選択できるため、プログラムの最適化を細かく管理できます。再帰や以前のバインディングを参照する機能が必要ない場合は、単純なletに頼って、パフォーマンスを少し節約できます。
これは、Schemeの段階的等式述部に似ています。(つまりeq?
、eqv?
およびequal?
)