型チェックと再帰型(Haskell / OcamlでのYコンビネータの記述)


21

HaskellのコンテキストでYコンビネーターを説明するとき、通常、単純な実装は再帰型のためHaskellで型チェックを行わないことに注意されています。

たとえば、Rosettacodeから:

The obvious definition of the Y combinator in Haskell canot be used
because it contains an infinite recursive type (a = a -> b). Defining
a data type (Mu) allows this recursion to be broken.

newtype Mu a = Roll { unroll :: Mu a -> a }

fix :: (a -> a) -> a
fix = \f -> (\x -> f (unroll x x)) $ Roll (\x -> f (unroll x x))

実際、「明白な」定義は型チェックを行いません:

λ> let fix f g = (\x -> \a -> f (x x) a) (\x -> \a -> f (x x) a) g

<interactive>:10:33:
    Occurs check: cannot construct the infinite type:
      t2 = t2 -> t0 -> t1
    Expected type: t2 -> t0 -> t1
      Actual type: (t2 -> t0 -> t1) -> t0 -> t1
    In the first argument of `x', namely `x'
    In the first argument of `f', namely `(x x)'
    In the expression: f (x x) a

<interactive>:10:57:
    Occurs check: cannot construct the infinite type:
      t2 = t2 -> t0 -> t1
    In the first argument of `x', namely `x'
    In the first argument of `f', namely `(x x)'
    In the expression: f (x x) a
(0.01 secs, 1033328 bytes)

Ocamlにも同じ制限があります。

utop # let fix f g = (fun x a -> f (x x) a) (fun x a -> f (x x) a) g;;
Error: This expression has type 'a -> 'b but an expression was expected of type 'a                                    
       The type variable 'a occurs inside 'a -> 'b

ただし、Ocamlでは、-rectypesスイッチを渡すことで再帰型を許可できます。

   -rectypes
          Allow  arbitrary  recursive  types  during type-checking.  By default, only recursive
          types where the recursion goes through an object type are supported.

を使用すると-rectypes、すべてが機能します。

utop # let fix f g = (fun x a -> f (x x) a) (fun x a -> f (x x) a) g;;
val fix : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b = <fun>
utop # let fact_improver partial n = if n = 0 then 1 else n*partial (n-1);;
val fact_improver : (int -> int) -> int -> int = <fun>
utop # (fix fact_improver) 5;;
- : int = 120

型システムと型推論に興味があるので、まだ答えられない質問がいくつかあります。

  • 最初に、タイプチェッカーはどのようにしてタイプを思い付きますt2 = t2 -> t0 -> t1か?そのタイプを思いついたので、問題はタイプ(t2)が右側のそれ自体を参照していることだと思いますか?
  • 第二に、そしておそらく最も興味深いのは、Haskell / Ocaml型システムがこれを許可しない理由は何ですか?Ocaml は、スイッチが与えられた場合、再帰型を処理できる場合でも、デフォルトでは許可しないため、十分な理由あると思います。-rectypes

これらが本当に大きなトピックである場合、関連する文献へのポインタをいただければ幸いです。

回答:


16

まず、GHCエラー、

GHCはいくつかの制約をxで統一しようとしています。まず、関数として使用します。

x :: a -> b

次に、その関数の値として使用します

x :: a

そして最後に、元の引数式でそれを統一します

x :: (a -> b) -> c -> d

今でx xは統一しようとしますが、の最初の引数であるでt2 -> t1 -> t0統一する必要があるため、これを統一することはできません。したがって、エラーメッセージが表示されます。t2xx

次に、一般的な再帰型ではありません。注目に値する最初のポイントは、等と再帰の型の違いです。

  • 等再帰性はmu X . Type、任意に展開または折りたたむこととまったく同じです。
  • ISO-再帰型は、オペレータのペアを提供する、foldそしてunfoldどのタイプの再帰的定義を折ると展開します。

等再帰型は理想的に聞こえますが、複雑な型のシステムで正しく動作するのはとてつもなく困難です。実際に型チェックを決定不能にすることができます。OCamlの型システムのすべての詳細に精通しているわけではありませんが、Haskellの完全に再帰的な型により、型チェッカーが型を統一しようとして勝手にループすることがあります。デフォルトでは、Haskellは型チェックが終了することを確認します。さらに、Haskellでは、型のシノニムは愚かで、最も便利な再帰型はのように定義されtype T = T -> ()ますが、Haskellではほとんどすぐにインライン化されますが、再帰型はインライン化できません、無限です!そのため、Haskellの再帰型では、同義語の処理方法を大幅に見直す必要がありますが、おそらく言語拡張としても努力する価値はありません。

アイソ再帰型は使用するのが少し面倒で、型チェッカーに型の折りたたみと展開の方法を明示的に指示する必要があり、プログラムの読み書きがより複雑になります。

ただし、これはあなたのMuタイプで行うことと非常に似ています。Roll折り畳まれ、unroll展開されます。したがって、実際にはiso-recursive型がベイクインされています。ただし、equa-recursive型は非常に複雑であるため、OCamlやHaskellなどのシステムでは、型レベルのフィックスポイントを介して繰り返しを渡す必要があります。

これに興味があれば、型とプログラミング言語をお勧めします。私はこれを書いているので、正しい用語を持っていることを確認するために、私のコピーは膝の上に開いて座っています:)


特に第21章では、誘導、coinduction、および再帰的なタイプのために良い勘を提供
ダニエルGratzer

ありがとうございました!これは本当に魅力的です。私は現在TAPLを読んでいますが、これについては本の後半で取り上げられることを嬉しく思います。
ベータ

@beta Yep、TAPLとその兄弟、型とプログラミング言語の高度なトピックは素晴らしいリソースです。
ダニエルグラッツァー

2

OCamlでは-rectypes、コンパイラーにパラメーターとして渡す(または#rectypes;;トップレベルで入力する)必要があります。大まかに言えば、これは統合中に「発生チェック」をオフにします。状況The type variable 'a occurs inside 'a -> 'bはもう問題になりません。型システムは依然として「正しい」(音など)であり、型として生じる無限ツリーは「合理的ツリー」と呼ばれることもあります。型システムは弱まります。つまり、プログラマのエラーを検出できなくなります。

OCamlの例での固定小数点演算子の詳細については、ラムダ計算に関する講義(スライド27以降)を参照してください。

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