Hindley-Milner型システムのみを使用してリストを定義する


10

Hindley-Milner型推論システムが機能し、再帰的レッツ(リンクされたコードではない)もサポートする小さなラムダ計算コンパイラーに取り組んでいます。これは、チューリングを完全にするのに十分であることを理解しています。

問題は、リストをサポートする方法がわからない、またはすでにリストをサポートしているかどうかわからないこと、そしてそれらをエンコードする方法を見つける必要があることだけです。型システムに新しいルールを追加する必要なく、それらを定義できるようにしたいと思います。

私はリストの考えることができる最も簡単な方法xのいずれかであるものとしてあるnull(または空のリスト)、またはANの両方が含まペアxとのリストをx。しかし、これを行うには、ペアとorを定義できる必要があります。

このようにペアを定義できるようです:

pair = λabf.fab
first = λp.p(λab.a)
second = λp.p(λab.b)

以来pairタイプを持つことになりa -> (b -> ((a -> (b -> x)) -> x))、通過した後、と言うintstring、それはタイプで何かを得たい(int -> (string -> x)) -> xのペアの表現であろう、intstring。ここで私が気になるのは、それがペアを表す場合、なぜそれが論理的に等価ではなく、命題を意味しないのint and stringかということです。ただし、と同等(((int and string) -> x) -> x)です。まるで、関数のパラメーターとして製品タイプしか持てないかのようです。この答えこの問題に対処しているようですが、彼が使用する記号の意味がわかりません。また、これが製品タイプを実際にエンコードしない場合、上記のペアの定義では実行できなかった製品タイプで実行できることはありますか(同じ方法でnタプルも定義できると考えて)?そうでない場合、これは含意のみを使用して(AFAIK)結合を表現できないという事実と矛盾しませんか?

また、合計タイプはどうですか?関数タイプのみを使用してなんとかしてエンコードできますか?もしそうなら、これはリストを定義するのに十分でしょうか?それとも、タイプシステムを拡張せずにリストを定義する他の方法はありますか?そうでない場合、できる限りシンプルにしたい場合、どのような変更を加える必要がありますか?

私はコンピュータープログラマーですが、コンピューターサイエンティストでも数学者でもないので、数学の表記を読むのはかなり苦手です。

編集: これまでに実装したものの技術的な名前はわかりませんが、基本的には、上でリンクしたコードのみです。これは、アプリケーション、抽象化、変数のルールを使用する制約生成アルゴリズムですHinley-Milnerアルゴリズムから、次に主要な型を取得する統合アルゴリズムから。例えば、式は\a.aタイプが得られるa -> a、と表現は、\a.(a a)チェックエラーが発生したスローされます。これに加えて、letルールはありませんが、次の疑似コードのような再帰的なグローバル関数を定義できる同じ効果を持つように見える関数があります。

GetTypeOfGlobalFunction(term, globalScope, nameOfFunction)
{
    // Here 'globalScope' contains a list of name-value pair where every value is of class 'ClosedType', 
    // meaning their type will be cloned before unified in the unification algorithm so that they can be used polymorphically 
    tempType = new TypeVariable() // Assign a dummy type to `tempType`, say, type 'x'.
    // The next line creates an scope with everything in 'globalScope' plus the 'nameOfFunction = tempType' name-value pair
    tempScope = new Scope(globalScope, nameOfFunction, tempType) 
    type = TypeOfTerm(term, tempScope) // Calculate the type of the term 
    Unify(tempType, type)
    return type
    // After returning, the code outside will create a 'ClosedType' using the returned type and add it to the global scope.
}

基本的に、コードは通常どおりに用語の型を取得しますが、統合する前に、ダミーの型で定義されている関数の名前を型スコープに追加して、それ自体から再帰的に使用できるようにします。

編集2:希望どおりのリストを定義するには、持っていない再帰型も必要になることに気づきました。


正確に何を実装したかについて、もう少し具体的に説明できますか?単純に型付けされたラムダ計算(再帰的な定義を含む)を実装し、パラメトリックな多態性Hindley-Milnerスタイルを与えましたか?または、2次多相ラムダ計算を実装しましたか?
Andrej Bauer

たぶん私はもっと簡単な方法で尋ねることができます:OCamlまたはSMLを取り、それを純粋なラムダ用語と再帰的な定義に制限すると、それはあなたが話していることですか?
Andrej Bauer

@AndrejBauer:質問を編集しました。OCamlとSMLについてはわかりませんが、Haskellを使用してラムダ項に制限し、最上位の再帰的レット(などlet func = \x -> (func x))を使用すると、私が持っているものを取得できるかどうかは確かです。
ファン

1
質問を改善するには、このメタ投稿をチェックしてください。
Juho

回答:


13

ペア

このエンコーディングは、ペアのチャーチエンコーディングです。同様の手法で、ブール値、整数、リスト、その他のデータ構造をエンコードできます。

x:a; y:bpair x y(a -> b -> t) -> t¬

(abt)t¬(¬a¬bt)t(ab¬t)t(ab)t
ab tpairt

pairペアタイプのコンストラクタで、firstかつsecondデストラクタです。(これらはオブジェクト指向プログラミングで使用されるのと同じ単語です。ここでの単語は、ここでは触れない型と用語の論理的な解釈に関連する意味を持っています。)直感的に、デストラクタを使用すると、オブジェクトとコンストラクターは、オブジェクトの一部に適用される関数を引数としてとることで、デストラクターへの道を開きます。この原理は他のタイプにも適用できます。

合計

差別された組合の教会エンコーディングは、ペアの教会エンコーディングに対して本質的に二重です。ペアに組み合わせる必要のある2つの部分があり、どちらか一方を抽出することを選択できる場合、2つの方法のいずれかでユニオンを構築することを選択でき、それを使用する場合は両方の方法を許可する必要があります。したがって、2つのコンストラクタがあり、2つの引数を取る単一のデストラクタがあります。

let case w = λf. λg. w f g           case : ((a->t) -> (b->t) -> t) -> (a->t) -> (b->t) -> t
  (* or simply let case w = w *)
let left x = λf. λg. f x             left : a -> ((a->t) -> (b->t) -> t)
let right y = λf. λg. g x            right : b -> ((a->t) -> (b->t) -> t)

タイプを(a->t) -> (b->t) -> tと省略してみましょうSUM(a,b)(t)。次に、デストラクタとコンストラクタのタイプは次のとおりです。

case : SUM(a,b)(t) -> (a->t) -> (b->t) -> t
left : a -> SUM(a,b)(t)
right : b -> SUM(a,b)(t)

したがって

case (left x) f g → f x
case (rightt y) f g → g y

リスト

リストについては、同じ原則を再度適用します。タイプaが要素のリストは、2つの方法で作成できます。空のリストにすることも、要素(先頭)とリスト(末尾)に追加することもできます。ペアと比較して、デストラクタに関しては少し工夫があります。2つの個別のデストラクタheadを持つことはできません。tailそれらは空でないリストでのみ機能するためです。2つの引数を持つ1つのデストラクタが必要です。1つはnilの場合の0引数関数(つまり値)で、もう1つはconsの場合の2引数関数です。のような関数is_emptyheadおよびtailそこから派生できます。合計の場合と同様に、リストは直接独自のデストラクタ関数です。

let nil = λn. λc. n
let cons h t = λn. λc. c h t
let is_empty l = l true (λh. λt. false) 
let head l default = l default (λh. λt. h)
let tail l default = l default (λh. λt. t)

consconsconsTT1,,Tn

ご想像のとおり、同種のリストのみを含むタイプを定義する場合は、再帰的なタイプが必要です。どうして?リストのタイプを見てみましょう。リストは、2つの引数をとる関数としてエンコードされます。空のリストで返す値と、コンスセルで返す値を計算する関数です。ましょうa、要素の型であるbリストのタイプである、とcデストラクタによって返されたタイプです。リストのタイプは

a -> (a -> b -> c) -> c

リストを同種にすることは、それがコンスセルの場合、テールは全体と同じタイプでなければならない、つまり制約を追加することを意味します

a -> (a -> b -> c) -> c = b

Hindley-Milner型システムは、このような再帰型で拡張することができ、実際のプログラミング言語はそれを行います。実用的なプログラミング言語は、このような「裸の」方程式を許可せず、データコンストラクターを必要とする傾向がありますが、これは基礎となる理論の本質的な要件ではありません。データコンストラクターを必要とすることで、型の推論が簡素化されます。実際には、実際にはバグのある関数が受け入れられない傾向がありますが、関数が使用されると、理解しにくい型エラーを引き起こす意図しない制約で型付けできてしまいます。これが、たとえば、OCamlがデフォルト以外の-rectypesコンパイラオプションでのみ保護されていない再帰型を受け入れる理由です。上記のOCaml構文での定義と、次の表記法を使用した同種リストの型定義エイリアスされた再帰型type_expression as 'atype_expressionが変数と統合されることを意味します'a

# let nil = fun n c -> n;;
val nil : 'a -> 'b -> 'a = <fun>
# let cons h t = fun n c -> c h t;;
val cons : 'a -> 'b -> 'c -> ('a -> 'b -> 'd) -> 'd = <fun>
# let is_empty l = l true (fun h t -> false);;
val is_empty : (bool -> ('a -> 'b -> bool) -> 'c) -> 'c = <fun>
# let head l default = l default (fun h t -> h);;
val head : ('a -> ('b -> 'c -> 'b) -> 'd) -> 'a -> 'd = <fun>
# let tail l default = l default (fun h t -> t);;
val tail : ('a -> ('b -> 'c -> 'c) -> 'd) -> 'a -> 'd = <fun>
# type ('a, 'b, 'c) ulist = 'c -> ('a -> 'b -> 'c) -> 'c;;
type ('a, 'b, 'c) ulist = 'c -> ('a -> 'b -> 'c) -> 'c
# is_empty (cons 1 nil);;
- : bool = false
# head (cons 1 nil) 0;;
- : int = 1
# head (tail (cons 1 (cons 2.0 nil)) nil) 0.;;
- : float = 2.

(* -rectypes is required for what follows *)
# type ('a, 'b, 'c) rlist = 'c -> ('a -> 'b -> 'c) -> 'c as 'b;;
type ('a, 'b, 'c) rlist = 'b constraint 'b = 'c -> ('a -> 'b -> 'c) -> 'c
# let rcons = (cons : 'a -> ('a, 'b, 'c) rlist -> ('a, 'b, 'c) rlist);;
val rcons :
  'a ->
  ('a, 'c -> ('a -> 'b -> 'c) -> 'c as 'b, 'c) rlist -> ('a, 'b, 'c) rlist =
  <fun>
# head (rcons 1 (rcons 2 nil)) 0;;
- : int = 1
# tail (rcons 1 (rcons 2 nil)) nil;;
- : 'a -> (int -> 'a -> 'a) -> 'a as 'a = <fun>
# rcons 1 (rcons 2.0 nil);;
Error: This expression has type
         (float, 'b -> (float -> 'a -> 'b) -> 'b as 'a, 'b) rlist = 'a
       but an expression was expected of type
         (int, 'b -> (int -> 'c -> 'b) -> 'b as 'c, 'b) rlist = 'c

折りたたみ

これをもう少し一般的に見ると、データ構造を表す関数は何ですか?

  • nn
  • (x,y)xy
  • ini(x)ix
  • [x1,,xn]

一般的に、データ構造はその折りたたみ関数として表されます。これはデータ構造の一般的な概念です。フォールド関数は、データ構造をトラバースする高次関数です。フォールドが普遍的であるという技術的な意味があります。すべての「ジェネリック」データ構造トラバーサルはフォールドに関して表現できます。データ構造をその折りたたみ関数として表すことができることは、これを示しています。データ構造について知っておく必要があるのは、データ構造をトラバースする方法だけで、残りは実装の詳細です。


あなたは整数、ペア、合計の「教会エンコーディング」に言及しますが、リストの場合はスコットエンコーディングを与えます。帰納的な型のエンコーディングに慣れていない人には少し混乱するかもしれません。
ステファン・ヒメネス

だから、基本的には、私のペアタイプは、実際には返すことができ、このタイプの関数として、製品の種類ではありませんt取ることになって議論をし、無視aしてb(正確である何(a and b) or tを言っています)。そして、私は合計で同じような問題を抱えているようです。また、再帰的な型がなければ、同種のリストはありません。簡単に言えば、同種のリストを取得するには、合計、積、および再帰型のルールを追加する必要があるとおっしゃっていますか?
ファン

もしかしてcase (right y) f g → g y、あなたの最後に和のセクション?
ファン

@StéphaneGimenez気づかなかった。私は、型付きの世界でこれらのエンコーディングに取り組むことに慣れていません。チャーチエンコーディングとスコットエンコーディングのリファレンスを教えてもらえますか?
Gilles「SO-邪悪なことをやめなさい」

@JuanLuisSoldi「おそらく、余分なレベルの間接参照で解決できない問題はない」と聞いたことがあるでしょう。チャーチエンコーディングは、関数呼び出しのレベルを追加することにより、データ構造を関数としてエンコードします。データ構造は、部品に作用する関数に適用する2次関数になります。同種のリストタイプが必要な場合は、テールのタイプがリスト全体のタイプと同じであるという事実に対処する必要があります。これには型の再帰が含まれる必要があると思います。
Gilles 'SO-悪をやめなさい'

2

合計タイプは、タグと値を持つ製品タイプとして表すことができます。この場合、少し騙して、1つのタグを使用してヌルかどうかを表し、2番目のタグがヘッド/テールのペアを表すようにします。

通常の方法でブール値を定義します。

true = λi.λe.i
false = λi.λe.e
if = λcond.λthen.λelse.(cond then else)

リストは、最初の要素がブール値であり、2番目の要素がヘッド/テールのペアであるペアです。いくつかの基本的なリスト関数:

isNull = λl.(first l)
null = pair false false     --The second element doesn't matter in this case
cons = λh.λt.(pair true (pair h t ))
head = λl.(fst (snd l))   --This is a partial function
tail = λl.(snd (snd l))   --This is a partial function  

map = λf.λl.(if (isNull l)
                 null 
                 (cons (f (head l)) (map f (tail l) ) ) 

しかし、これは私に同質のリストを与えません、これは正しいですか?
ファン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.