Ocaml / F#の関数がデフォルトで再帰的でないのはなぜですか?


104

F#およびOcaml(およびおそらく他の言語)の関数がデフォルトで再帰的でないのはなぜですか?

言い換えると、なぜ言語設計者は、次のrecような宣言を明示的に入力させるのが良い考えだと判断したのですか。

let rec foo ... = ...

デフォルトで関数の再帰機能を提供しないのですか?なぜ明示的なrec構成が必要なのですか?


回答:


87

元の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バインドされた関数はデフォルトで再帰的です。ほとんどの関数定義が関数名の以前のバインディングにアクセスする必要がない場合、これによりコードが単純になります。しかし、代わら機能は、異なる名前(使用するように作られていますf1f2など)のスコープを汚染し、誤っ機能の間違った「バージョン」呼び出しすることが可能になるました。また、暗黙的に再帰的にfunバインドされた関数と非再帰的にバインドされた関数との間に不一致がありvalます。

Haskellは、定義を純粋に制限することにより、定義間の依存関係を推測することを可能にします。これにより、おもちゃのサンプルはシンプルに見えますが、他の場所では重大なコストがかかります。

ガネーシュとエディによって与えられた答えは赤いニシンであることに注意してください。彼らはlet rec ... and ...、型変数がいつ一般化されるかに影響を与えるため、関数のグループを巨人の中に配置できない理由を説明しました。これはrec、SMLのデフォルトであることとは関係ありませんが、OCaml とは関係ありません。


3
私はそれらが赤いニシンだとは思わない:推論の制限がなければ、プログラムやモジュール全体が他のほとんどの言語のように自動的に相互再帰として扱われる可能性が高い。これにより、「rec」を必要とするかどうかを具体的に設計する必要があります。
GS-

「...他のほとんどの言語のように、相互再帰として自動的に処理されます」。BASIC、C、C ++、Clojure、Erlang、F#、Factor、Forth、Fortran、Groovy、OCaml、Pascal、Smalltalk、および標準MLにはありません。
JD

3
C / C ++は、前方定義のプロトタイプのみを必要とします。これは、再帰を明示的にマークすることではありません。Java、C#、Perlは確かに暗黙の再帰を行います。「ほとんど」の意味と各言語の重要性については、無限の議論に巻き込まれる可能性があります。そこで、「非常に多くの」他の言語に取り掛かりましょう。
GS-モニカに謝罪

3
「C / C ++は、前方定義のプロトタイプのみを必要とします。これは、再帰を明示的にマークすることではありません。」自己再帰の特別な場合のみ。相互再帰の一般的なケースでは、CとC ++の両方で前方宣言が必須です。
JD

2
実際には、C ++のクラススコープでは前方宣言は必要ありません。つまり、静的メソッドは宣言なしで互いに呼び出すことができます。
polkovnikov.ph

52

を明示的に使用する重要な理由の1つrecは、すべての静的に型付けされた関数型プログラミング言語の基礎となるHindley-Milner型推論を行うことです(さまざまな方法で変更および拡張されます)。

あなたが定義を持っている場合let f x = x、あなたはそれが型を持つことを期待したい'a -> 'aと異なるの適用可能と'a異なる点でタイプ。しかし、同様に、あなたがを書いた場合let g x = (x + 1) + ...、あなたはの体の残りの部分xとして扱われることを期待するでしょう。intg

Hindley-Milner推論がこの区別を処理する方法は、明示的な一般化ステップによるものです。プログラムを処理する特定の時点で、型システムは停止し、「OK、これらの定義の型はこの時点で一般化されます。そのため、誰かがそれらを使用すると、その型の任意の自由型変数が新たにインスタンス化されるため、この定義の他の使用を妨げることはありません。」

この一般化を行うための賢明な場所は、相互に再帰的な関数のセットをチェックした後です。以前は、一般化しすぎて、型が実際に衝突する可能性がある状況につながります。後で、一般化しすぎて、複数の型のインスタンス化で使用できない定義を作成します。

では、型チェッカーが相互に再帰的な定義のセットについて知る必要がある場合、何ができるでしょうか?1つの可能性は、スコープ内のすべての定義に対して依存関係分析を行い、それらを可能な限り最小のグループに並べ替えることです。Haskellは実際にこれを行いますが、F#(およびOCamlやSML)などの制限のない副作用がある言語では、副作用も並べ替えられる可能性があるため、これは悪い考えです。したがって、代わりに、どの定義が相互に再帰的であるかを明示的にマークするようにユーザーに要求します。したがって、一般化が発生する必要がある拡張によって。


3
いえいえ。最初の段落が間違っている(「rec」ではなく「and」の明示的な使用について話している)ため、残りは無関係です。
JD、

5
私はこの要件に決して満足していませんでした。説明ありがとう。Haskellがデザインに優れているもう1つの理由。
Bent Rasmussen、

9
番号!!!!これはどのようにして起こりましたか?!この答えは間違いです!以下のHarropの回答を読むか、The Standard of Standard ML(Milner、Tofte、Harper、MacQueen-1997)[p.24]
lambdapower

9
回答で述べたように、型推論の問題は、recが唯一の理由ではなく、recを必要とする理由の1つです。Jonの回答も非常に有効な回答です(Haskellに関する通常のわくわくコメントは別として)。私は両者が対立しているとは思わない。
GS-

16
「型推論の問題は、recが必要な理由の1つです」。OCamlが必要とするrecがSMLは必要としないという事実は、明白な反例です。あなたが説明する理由のために型推論が問題であるならば、OCamlとSMLは彼らがしたように異なるソリューションを選択できなかったでしょう。もちろんその理由は、andHaskellを適切にするためにあなたが話しているからです。
JD

10

これが良いアイデアである主な理由は2つあります。

まず、再帰的な定義を有効にすると、同じ名前の値の以前のバインディングを参照できなくなります。これは、既存のモジュールを拡張するような場合に便利なイディオムです。

第2に、再帰的な値、特に相互再帰的な値のセットは、定義が順番に処理されることを考えるのがはるかに難しく、新しい定義はそれぞれ、すでに定義されているものの上に構築されます。そのようなコードを読むときに、再帰的として明示的にマークされた定義を除いて、新しい定義は以前の定義のみを参照できることが保証されていると便利です。


4

いくつかの推測:

  • let関数のバインドだけでなく、他の通常の値にも使用されます。ほとんどの形式の値は、再帰的であってはなりません。特定の形式の再帰的な値(たとえば、関数、遅延式など)が許可されているため、これを示す明示的な構文が必要です。
  • 非再帰関数を最適化する方が簡単かもしれません
  • 再帰関数を作成するときに作成されるクロージャーには、関数自体を指すエントリを含める必要があります(そのため、関数はそれ自体を再帰的に呼び出すことができます)。これにより、再帰クロージャーは非再帰クロージャーよりも複雑になります。したがって、再帰が不要な場合は、より単純な非再帰的なクロージャーを作成できると便利です。
  • これにより、以前に定義された関数または同じ名前の値に関して関数を定義できます。これは悪い習慣だと思いますが
  • 余分な安全?あなたが意図したことをしていることを確認します。たとえば、再帰的にするつもりはないが、関数内で関数自体と同じ名前の名前を誤って使用した場合、(名前が以前に定義されていない限り)ほとんどの場合、文句を言うでしょう。
  • このlet構成はlet、LispおよびSchemeの構成に似ています。非再帰的です。別にありますletrec再帰的なLet'sのSchemeに構成要素

「ほとんどの値の形式は再帰的には許可されていません。特定の形式の再帰的な値(たとえば、関数、遅延式など)が許可されているため、これを示すために明示的な構文が必要です。」これは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したものをに適用gした結果に適用するように再定義しますa。後者は再定義しますfに適用gして永久にループするようにしますがa、これは通常、MLバリアントでは必要ありません。

とはいえ、それは言語デザイナーのようなものです。そのままにしてください。


1

その大きな部分は、プログラマがローカルスコープの複雑さをより細かく制御できることです。スペクトルletlet*及びlet rec電力及びコストの両方の増加レベルを提供します。let*そしてlet rec本質的にシンプルな入れ子バージョンですletいずれかを使用すると、より高価であるので、。このグレーディングを使用すると、現在のタスクに必要なletのレベルを選択できるため、プログラムの最適化を細かく管理できます。再帰や以前のバインディングを参照する機能が必要ない場合は、単純なletに頼って、パフォーマンスを少し節約できます。

これは、Schemeの段階的等式述部に似ています。(つまりeq?eqv?およびequal?

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.