回答:
誰か∀X
が言っているユニバーサルタイプを定義すると、次のようになります:好きなタイプをプラグインできX
ます。私の仕事をするためにタイプについて何も知る必要はありません。それを不透明に参照するだけです。
誰かが存在タイプ∃X
を定義するとき、彼らは言っています:私はここで私が欲しいどんなタイプでも使います; あなたはタイプについて何も知らないので、あなたはそれを不透明に参照することができるだけですX
。
ユニバーサルタイプを使用すると、次のように記述できます。
void copy<T>(List<T> source, List<T> dest) {
...
}
copy
機能はないアイデアを持っていないものをT
、実際になりますが、それはする必要はありません。
存在するタイプは、次のようなものを書くことができます:
interface VirtualMachine<B> {
B compile(String source);
void run(B bytecode);
}
// Now, if you had a list of VMs you wanted to run on the same input:
void runAllCompilers(List<∃B:VirtualMachine<B>> vms, String source) {
for (∃B:VirtualMachine<B> vm : vms) {
B bytecode = vm.compile(source);
vm.run(bytecode);
}
}
リスト内の各仮想マシン実装は、異なるバイトコードタイプを持つことができます。runAllCompilers
この関数は、バイトコードの種類が何であるかわかりませんが、それはする必要はありません。それがするすべてはからVirtualMachine.compile
にバイトコードをリレーすることですVirtualMachine.run
です。
Javaタイプのワイルドカード(例: List<?>
は、存在タイプの非常に限定された形式です。
更新:ユニバーサルタイプを使用して存在タイプをシミュレートできることを忘れてしまいました。まず、ユニバーサル型をラップして、型パラメーターを非表示にします。次に、制御を反転します(これにより、上記の定義の「あなた」と「I」の部分が効果的に入れ替わります。これは、存在と普遍の主な違いです。)
// A wrapper that hides the type parameter 'B'
interface VMWrapper {
void unwrap(VMHandler handler);
}
// A callback (control inversion)
interface VMHandler {
<B> void handle(VirtualMachine<B> vm);
}
これで、普遍的に型付けされた関数を持つVMWrapper
独自の呼び出しを作成できます。正味の効果は同じで、コードは不透明として処理する必要があります。VMHandler
handle
B
void runWithAll(List<VMWrapper> vms, final String input)
{
for (VMWrapper vm : vms) {
vm.unwrap(new VMHandler() {
public <B> void handle(VirtualMachine<B> vm) {
B bytecode = vm.compile(input);
vm.run(bytecode);
}
});
}
}
VMの実装例:
class MyVM implements VirtualMachine<byte[]>, VMWrapper {
public byte[] compile(String input) {
return null; // TODO: somehow compile the input
}
public void run(byte[] bytecode) {
// TODO: Somehow evaluate 'bytecode'
}
public void unwrap(VMHandler handler) {
handler.handle(this);
}
}
List<∃B:VirtualMachine<B>> vms
またはの数学的表記の意味が完全に理解できませんfor (∃B:VirtualMachine<B> vm : vms)
。(これらはジェネリック型なので?
、「自作」構文の代わりにJavaのワイルドカードを使用できませんでしたか?)などのジェネリック型が含まれ∃B:VirtualMachine<B>
ていないが、代わりに「ストレート」なコード例があると役立つと思います。∃B
なぜなら、ジェネリック型は最初のコード例の後で簡単にユニバーサル型に関連付けられるからです。
∃B
は以前、定量化が行われている場所を明確に示していました。ワイルドカード構文では、量指定子が暗黙に含まれます(List<List<?>>
実際にはを意味し∃T:List<List<T>>
、を意味しませんList<∃T:List<T>>
)。また、明示的な数量化によって型を参照できます(この例を変更して、型のバイトコードをB
一時変数に格納することでこれを活用しています)。
値実存型などが∃x. F(x)
ペアであるいくつかの含有タイプ x
及び値タイプのをF(x)
。以下のような多相型の値に対し、∀x. F(x)
ある機能ある種かかりx
且つ生産タイプの値F(x)
。どちらの場合も、型はいくつかの型コンストラクターで閉じますF
。
このビューではタイプと値が混在していることに注意してください。存在証明は、1つのタイプと1つの値です。普遍的な証明は、タイプ(またはタイプから値へのマッピング)でインデックス付けされた値のファミリー全体です。
したがって、指定した2つのタイプの違いは次のとおりです。
T = ∃X { X a; int f(X); }
つまり、typeの値にT
は、という型X
、value a:X
、およびfunction が含まれますf:X->int
。タイプの値のプロデューサは、任意のタイプT
を選択できX
、コンシューマはについて何も知ることができませんX
。その一つの例があると呼ばれることを除いてa
、この値をに変えることができることをint
にそれを与えることによってf
。つまり、typeの値はT
、int
どういうわけか生成する方法を知っています。さて、私たちは中間型X
を排除し、ただ次のように言うことができます:
T = int
普遍的に定量化されたものは少し異なります。
T = ∀X { X a; int f(X); }
つまり、typeの値にはT
任意のtypeを指定できX
、値が何であっても、値a:X
と関数を生成します。つまり、タイプの値のコンシューマは、の任意のタイプを選択できます。また、タイプの値のプロデューサは、について何も知ることができませんが、の任意の選択に対して値を生成でき、そのような値をに変換できる必要がありますf:X->int
X
T
X
T
X
a
X
int
。
考えられるすべての型の値を生成できるプログラムがないため、この型の実装は明らかに不可能です。あなたが不条理を許可しない限りnull
または底の。
実存論はペアであるため、カリー化によって実存論の引数を普遍的なものに変換できます。
(∃b. F(b)) -> Int
と同じです:
∀b. (F(b) -> Int)
前者は、ランク2の存在です。これにより、次の便利なプロパティが得られます。
すべての実存的に定量化されたタイプのランク
n+1
は、普遍的に定量化されたタイプのランクn
です。
Skolemizationと呼ばれる、存在をユニバーサルに変換するための標準アルゴリズムがあります。
2つの概念は相補的であるため、つまり、一方が他方の「反対」であるため、存在型をユニバーサル型と一緒に説明することには意味があると思います。
実存型についてのすべての詳細(正確な定義を与える、考えられるすべての使用法、抽象データ型との関係などをリストするなど)に答えることはできません。このHaskellWiki記事が実存型の主な効果であると述べていることだけを(Javaを使用して)示します。
存在タイプは、いくつかの異なる目的に使用できます。しかし、彼らがしていることは、右側の型変数を「隠す」ことです。通常、右側に表示される型変数は左側にも表示される必要があります[…]
設定例:
次の疑似コードは、それを修正するのは簡単ですが、Javaとしては有効ではありません。実際、それがまさに私がこの答えでやろうとしていることです!
class Tree<α>
{
α value;
Tree<α> left;
Tree<α> right;
}
int height(Tree<α> t)
{
return (t != null) ? 1 + max( height(t.left), height(t.right) )
: 0;
}
これについて簡単に説明します。定義しています…
Tree<α>
二分木のノードを表す再帰型。各ノードvalue
はあるタイプの αを、同じタイプのオプションleft
およびright
サブツリーへの参照を持っています。
height
葉ノードからルートノードまでの最遠距離を返す関数t
。
さて、上の疑似コードを height
適切なJava構文に!(簡潔にするために、オブジェクト指向やアクセシビリティ修飾子などのボイラープレートは省略し続けます。)2つの可能な解決策を示します。
1.ユニバーサルタイプソリューション:
最も明白な修正はheight
、型パラメーターαをシグニチャーに導入することにより、単純にジェネリックにすることです。
<α> int height(Tree<α> t)
{
return (t != null) ? 1 + max( height(t.left), height(t.right) )
: 0;
}
これにより、変数を宣言し、タイプαの式を作成できます。応じて、、その関数内。だが...
2.実存型ソリューション:
メソッドの本体を見ると、タイプαの何かに実際にアクセスしたり、操作したりしていないことがわかります。その型を持つ式も、その型で宣言された変数もないので、なぜheight
総称にする必要があるのでしょうか。なぜαを単純に忘れられないのですか?結局のところ、次のことが可能です。
int height(Tree<?> t)
{
return (t != null) ? 1 + max( height(t.left), height(t.right) )
: 0;
}
この回答の冒頭で書いたように、実存型と普遍型は本質的に相補的/二重です。したがって、普遍的な型の解決策がheight
よりジェネリックにすることである場合、存在型は逆の効果があることを期待する必要があります:ジェネリックを少なくする、つまり型パラメーターαを非表示 /削除する。
その結果、t.value
識別子がバインドされていないため、このメソッドでの型を参照したり、その型の式を操作したりすることができなくなります。(?
ワイルドカードは特殊なトークンであり、タイプを「キャプチャーする」識別子ではありません。)t.value
事実上不透明になりました。おそらくそれを使ってできる唯一のことは、それを型キャストすることですObject
です。
概要:
===========================================================
| universally existentially
| quantified type quantified type
---------------------+-------------------------------------
calling method |
needs to know | yes no
the type argument |
---------------------+-------------------------------------
called method |
can use / refer to | yes no
the type argument |
=====================+=====================================
Object
は非常に興味深いです:どちらも静的に型に依存しないコードを記述できるという点で似ていますが、前者(ジェネリック)は利用可能な型情報のほとんどすべてを破棄するだけではありませんこの目標を達成する。この特定の意味では、ジェネリック薬はObject
IMOの改善策です。
public static void swap(List<?> list, int i, int j) { swapHelper(list, i, j); } private static <E> void swapHelper(List<E> list, int i, int j) { list.set(i, list.set(j, list.get(i))); }
、E
あるuniversal type
と?
表しますかexistential type
?
?
型のinはint height(Tree<?> t)
関数内ではまだ不明であり、渡すツリーを選択するのは呼び出し元であるため、呼び出し元によって決定されます。Javaでこれを存在型と呼んでも違います。?
プレースホルダができるいくつかの状況では、Javaでexistentialsのフォームを実装するために使用されるが、これはその一つではありません。
これらはすべて良い例ですが、少し違った方法で答えることにしました。数学から思い出してください。P(x)は、「すべてのxについて、P(x)であることを証明できる」という意味です。言い換えれば、それは一種の関数であり、あなたは私にxを与え、私はあなたのためにそれを証明する方法を持っています。
型理論では、証明についてではなく、型について話します。したがって、このスペースでは、「あなたが私に与えるどんなタイプXについても、私はあなたに特定のタイプPを与える」という意味です。さて、PはXについて、それが型であるという事実を除いて、あまり情報を提供しないので、PはXについてはあまり実行できませんが、いくつかの例があります。Pは、「同じタイプのすべてのペア」のタイプを作成できますP<X> = Pair<X, X> = (X, X)
。または、オプションtype:を作成することもできますP<X> = Option<X> = X | Nil
。ここで、Nilはnullポインターのタイプです。それからリストを作ることができます:List<X> = (X, List<X>) | Nil
。最後のList<X>
要素は再帰的であり、の値は、最初の要素がXで2番目の要素がaであるペア、List<X>
またはnullポインタであることに注意してください。
今、数学∃xで。P(x)は、「P(x)が真になるような特定のxがあることを証明できる」ことを意味します。そのようなxはたくさんあるかもしれませんが、それを証明するには1つで十分です。それを考える別の方法は、空ではない証拠と証明のペアのセット{(x、P(x))}が存在しなければならないということです。
型理論に変換:ファミリー∃X.P<X>
の型は、型Xと対応する型P<X>
です。XをPに与える前に(Xについてはすべて知っているが、Pはほとんど知らないように)、反対が今は真であることに注意してください。P<X>
Xに関する情報を提供することを約束するものではありません。Xが存在し、それが実際に型であるということだけです。
これはどのように役立ちますか?ええと、Pはその内部型Xを公開する方法を持つ型である可能性があります。例として、その状態Xの内部表現を非表示にするオブジェクトがあります。直接操作する方法はありませんが、その効果を観察することができます。 Pを突っ込んでください。このタイプの実装は多数ある可能性がありますが、どのタイプを選択しても、これらすべてのタイプを使用できます。
P<X>
代わりであることを知ることによって、関数は何を得るのですかP
(同じ機能とコンテナータイプ、たとえば、それが含まれていることを知らないX
)?
∀x. P(x)
の証明可能性については何も意味せずP(x)
、真実のみを意味します。
質問に直接回答するには:
ユニバーサルタイプでは、の使用にT
タイプパラメータを含める必要がありますX
。たとえばT<String>
またはT<Integer>
。存在する型のT
場合、その型パラメーターは不明または無関係であるため、使用しないでください。使用するのはT
(またはJavaでT<?>
)だけです。
さらに詳しい情報:
ユニバーサル/抽象型と実存型は、オブジェクト/関数のコンシューマー/クライアントとそのプロデューサー/実装の間の二重の視点です。一方が普遍的なタイプを見るとき、もう一方は存在タイプを見る。
Javaでは、ジェネリッククラスを定義できます。
public class MyClass<T> {
// T is existential in here
T whatever;
public MyClass(T w) { this.whatever = w; }
public static MyClass<?> secretMessage() { return new MyClass("bazzlebleeb"); }
}
// T is universal from out here
MyClass<String> mc1 = new MyClass("foo");
MyClass<Integer> mc2 = new MyClass(123);
MyClass<?> mc3 = MyClass.secretMessage();
MyClass
、T
あなたがのために任意の型に置き換えることができますので、普遍的ですT
あなたは、そのクラスを使用するときに、あなたがのインスタンスを使用するたびに、Tの実際の型を知っている必要がありますMyClass
MyClass
自体の観点から、T
、実際のタイプがわからないため存在T
?
、存在型を表します-したがって、クラスの内部にいるときT
は基本的に?
です。のインスタンスMyClass
をT
existentialで処理する場合は、上記MyClass<?>
のsecretMessage()
例のように宣言できます。他の場所で説明されているように、存在するタイプは、何かの実装の詳細を隠すために使用されることがあります。このJavaバージョンは次のようになります。
public class ToDraw<T> {
T obj;
Function<Pair<T,Graphics>, Void> draw;
ToDraw(T obj, Function<Pair<T,Graphics>, Void>
static void draw(ToDraw<?> d, Graphics g) { d.draw.apply(new Pair(d.obj, g)); }
}
// Now you can put these in a list and draw them like so:
List<ToDraw<?>> drawList = ... ;
for(td in drawList) ToDraw.draw(td);
これを適切にキャプチャするのは少し難しいです。Javaがそうではない、ある種の関数型プログラミング言語にいるふりをしているからです。しかし、ここでのポイントは、ある種の状態とその状態を操作する関数のリストをキャプチャしていて、状態部分の実際のタイプがわからないということですが、関数はすでにそのタイプと一致しているため、 。
現在、Javaでは、すべての非最終的な非プリミティブ型が部分的に存在しています。これは奇妙に聞こえるかもしれませObject
んが、として宣言された変数が潜在的にObject
代わりにのため、特定の型を宣言することはできず、「この型またはサブクラス」のみを宣言できます。したがって、オブジェクトは状態のビットとその状態で動作する関数のリストとして表されます-呼び出す関数は実行時にルックアップによって決定されます。これは、存在状態の部分とその状態を操作する関数がある場合の上記の存在型の使用によく似ています。
サブタイプやキャストのない静的に型付けされたプログラミング言語では、存在型を使用すると、型の異なるオブジェクトのリストを管理できます。のリストにT<Int>
を含めることはできませんT<Long>
。ただし、のリストにT<?>
はのバリエーションを含めることができ、T
さまざまな種類のデータをリストに入れて、それらをすべてintに変換する(またはデータ構造内で提供される操作を実行する)ことができます。
ほとんどの場合、存在型のレコードをクロージャーを使用せずにレコードに変換できます。クロージャも実在的に型付けされており、閉じられた自由変数は呼び出し元から隠されています。したがって、クロージャをサポートするが存在型をサポートしない言語を使用すると、オブジェクトの存在部分に入れたのと同じ隠された状態を共有するクロージャを作成できます。
存在型は不透明な型です。
Unixのファイルハンドルについて考えてみます。あなたはその型がintであることを知っているので、簡単に偽造することができます。たとえば、ハンドル43から読み取ろうとすることができます。プログラムがこの特定のハンドルでファイルを開いている場合は、そこから読み取ります。あなたのコードは悪質である必要はありません、ずさんなだけです(例えば、ハンドルは初期化されていない変数であるかもしれません)。
存在タイプはプログラムから隠されています。fopen
存在タイプが返された場合、それを使用してできることは、この存在タイプを受け入れるいくつかのライブラリ関数で使用することだけです。たとえば、次の疑似コードはコンパイルされます。
let exfile = fopen("foo.txt"); // No type for exfile!
read(exfile, buf, size);
インターフェース「read」は次のように宣言されています。
次のようなタイプTが存在します。
size_t read(T exfile, char* buf, size_t size);
変数exfileは、int、a char*
、struct Fileではなく、型システムで表現できるものではありません。型が不明な変数を宣言することはできず、ポインタをその不明な型にキャストすることもできません。言語はあなたをさせません。
read
が∃T.read(T file, ...)
次にある場合、最初のパラメーターとして渡すことができるものはありません。何が仕事を持っているだろうfopen
ファイルハンドルを返すと、 read関数と同じ実存によってスコープ:∃T.(T, read(T file, ...))
私は少し遅れているようですが、とにかく、このドキュメントは存在タイプが何であるかについての別の見方を追加します、特に言語に依存しませんが、存在タイプを理解することはかなり簡単になるはずです: .nl / groups / ST / Projects / ehc / ehc-book.pdf(第8章)
普遍的と実存的に定量化されたタイプの違いは、次の観察によって特徴付けることができます。
∀数量化タイプの値を使用すると、数量化タイプ変数のインスタンス化に選択するタイプが決まります。たとえば、識別関数「id ::∀aa→a」の呼び出し元は、この特定のidアプリケーションのタイプ変数aに選択するタイプを決定します。関数アプリケーション「id 3」の場合、このタイプはIntと同じです。
∃数量化タイプの値を作成すると、数量化タイプ変数のタイプが決定され、非表示になります。たとえば、「∃a。(a、a→Int)」の作成者は、「(3、λx→x)」からその型の値を作成した可能性があります。別の作成者が「( 'x'、λx→ord x)」から同じ型の値を作成しました。ユーザーの観点から見ると、両方の値は同じタイプであり、したがって交換可能です。値には、型変数aに対して選択された特定の型がありますが、どの型かわからないため、この情報を利用することはできません。この値固有のタイプ情報は「忘れられました」。私たちはそれが存在することを知っているだけです。
タイプパラメータのすべての値にユニバーサルタイプが存在します。存在タイプは、存在タイプの制約を満たすタイプパラメータの値に対してのみ存在します。
たとえば、Scalaで存在型を表現する1つの方法は、いくつかの上限または下限に制約されている抽象型です。
trait Existential {
type Parameter <: Interface
}
同様に、制約付きユニバーサルタイプは、次の例のように存在タイプです。
trait Existential[Parameter <: Interface]
のInterface
インスタンス化可能なサブタイプはExistential
、type Parameter
を実装する必要があるを定義する必要があるため、どの使用サイトでもを使用できますInterface
。
縮重場合 Scalaで実存タイプのが参照されることはないので、任意のサブタイプによって定義される必要はない抽象型です。これは事実上List[_]
、ScalaとList<?>
Javaの略記です。
私の答えは、抽象型と実存型を統合するというマーティンオデルスキーの提案に触発されました。付属のスライドは、理解の助けとなります。
∀x.f(x)
受信機能に対して不透明であり∃x.f(x)
、存在タイプは特定のプロパティを持つように制限されています。通常、すべてのパラメーターは、その機能が直接操作するため、存在します。しかし、一般的なパラメータは、関数は、そのようなのような参照を取得するなどの基本的なユニバーサル操作を超えて、それらを管理しませんので、ユニバーサルのあるタイプを持っている可能性があります∀x.∃array.copy(src:array[x] dst:array[x]){...}
forSome
型パラメーターの存在量の定量化があります。
抽象データ型と情報隠蔽の研究により、実存型がプログラミング言語に取り入れられました。データ型を抽象化すると、その型に関する情報が非表示になるため、その型のクライアントはそれを悪用することはできません。オブジェクトへの参照を持っているとしましょう...一部の言語では、その参照をバイトへの参照にキャストして、そのメモリの一部に対して必要なことを何でも実行できます。プログラムの動作を保証するために、オブジェクトの設計者が提供するメソッドを介して、オブジェクトへの参照のみを操作することを言語が強制すると便利です。タイプが存在することはわかっていますが、それ以上はありません。
見る:
抽象型には存在型、MITCHEL&PLOTKINがあります