ウィキペディアの記事既存のタイプを読みました。存在演算子(∃)のために、それらが存在型と呼ばれることを集めました。しかし、そのポイントが何であるかはわかりません。の違いは何ですか
T = ∃X { X a; int f(X); }
そして
T = ∀x { X a; int f(X); }
?
誰かがユニバーサルタイプ∀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<?>
)は、存在タイプの非常に限られた形式です。
更新:ユニバーサル型を使用して実在型をシミュレートできることを忘れていました。最初に、ユニバーサル型をラップして、型パラメーターを非表示にします。次に、制御を反転します(これにより、上記の定義の「あなた」と「私」の部分が事実上入れ替わります。これは、実存と普遍の主な違いです)。
// 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);
}
}
∃x. F(x)
のような実在型の値はペアいくつかのtypex
およびタイプF(x)
の-valueを含む。 ∀x. F(x)
のようなポリモーフィック型の値はfunctionで、x
型を取り、producesF(x)
型の値を取ります。どちらの場合も、型はいくつかの型コンストラクターF
を閉じます。
このビューにはタイプと値が混在していることに注意してください。存在証明は、1つのタイプと1つの値です。ユニバーサル証明は、タイプ(またはタイプから値へのマッピング)でインデックス付けされた値のファミリ全体です。
したがって、指定した2つのタイプの違いは次のとおりです。
T = ∃X { X a; int f(X); }
つまり、タイプT
の値には、X
と呼ばれるタイプ、値a:X
、および関数f:X->int
が含まれます。タイプT
の値のプロデューサーは、X
のanyタイプを選択し、コンシューマーはX
について何も知ることができません。 a
という名前の例が1つあり、この値をint
に渡すことでf
に変換できることを除きます。つまり、タイプT
の値は、何らかの方法でint
を生成する方法を知っています。さて、中間タイプX
を削除して、次のように言うことができます。
T = int
普遍的に定量化されたものは少し異なります。
T = ∀X { X a; int f(X); }
つまり、タイプT
の値には、任意のタイプX
を与えることができ、値a:X
と関数f:X->int
X
が何であってもを生成します。つまり、タイプT
の値のコンシューマーは、X
の任意のタイプを選択できます。タイプT
の値のプロデューサーは、X
についてまったく何も知ることができませんが、a
の任意の選択に対して値X
を生成し、そのような値をint
に変換できる必要があります。
考えられるすべての型の値を生成できるプログラムがないため、明らかにこの型を実装することは不可能です。 null
やbottomsのような不条理を許可しない限り。
存在はペアであるため、存在引数はcurryingを介してユニバーサル引数に変換できます。
(∃b. F(b)) -> Int
次と同じです:
∀b. (F(b) -> Int)
前者はrank-2存在です。これにより、次の有用なプロパティが得られます。
実存的に数量化されたランク
n+1
はすべて、ランクn
の普遍的に数量化されたタイプです。
Skolemization と呼ばれる、実在物を普遍化する標準的なアルゴリズムがあります。
2つの概念は相補的であるため、つまり、一方が他方の「反対」であるため、実在型と普遍型を説明することは理にかなっていると思います。
存在タイプについての詳細(正確な定義の提示、可能なすべての使用のリスト、抽象データタイプとの関係など)に答えることはできません。 (Javaを使用して) このHaskellWikiの記事 が存在型の主な効果であることを示すだけです。
既存のタイプは、いくつかの異なる目的に使用できます。しかし、彼らが行うことは右側の型変数を「隠す」ことです。通常、右側に現れる型変数は左側にも現れる必要があります[…]
設定例:
次の擬似コードは、それを修正するのに十分簡単であっても、まったく有効な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
moreをジェネリックにすることである場合、実在型には逆の効果があると期待する必要があります:less汎用、つまり、型パラメーターαの非表示/削除による。
結果として、識別子がバインドされていないため、このメソッドで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 |
=====================+=====================================
これらはすべて良い例ですが、少し違った答えをすることにします。数学から思い出してください。 P(x)は、「すべてのxについて、P(x)であることを証明できる」ことを意味します。言い換えれば、それは一種の関数であり、xを与え、メソッドがあります。あなたのためにそれを証明します。
型理論では、証明についてではなく、型について話しています。したがって、このスペースでは、「あなたが私に与えるすべてのタイプXに対して、特定のタイプPを提供します」という意味です。さて、Xが型であるという事実以外にXについて多くの情報をPに与えないので、PはXでそれを行うことができませんが、いくつかの例があります。 Pは、「同じタイプのすべてのペア」のタイプを作成できます:P<X> = Pair<X, X> = (X, X)
。または、オプションタイプを作成することができます:_P<X> = Option<X> = X | Nil
_、ここでNilはNULLポインターのタイプです。それからリストを作成できます:List<X> = (X, List<X>) | Nil
。最後の要素は再帰的であり、_List<X>
_の値は、最初の要素がXで2番目の要素が_List<X>
_のペアであるか、またはNULLポインタであることに注意してください。
今、数学で∃x。 P(x)は、「P(x)が真であるような特定のxがあることを証明できる」ことを意味します。別の考え方として、空ではない証拠と証明のペア{(x、P(x))}のセットが存在する必要があるということです。
型理論への変換:ファミリー_∃X.P<X>
_の型はX型であり、対応する型_P<X>
_です。 XをPに渡す前に(Xについてはすべて知っていたが、Pについてはほとんど知らなかったように)、今ではその反対が成り立っていることに注意してください。 _P<X>
_は、Xに関する情報を提供することを約束していません。Xが存在すること、そしてそれが実際に型であることだけを保証します。
これはどのように役立ちますか? Pは、その内部型Xを公開する方法を持つ型です。例としては、状態Xの内部表現を隠すオブジェクトがあります。それを直接操作する方法はありませんが、このタイプの実装は多数存在する可能性がありますが、選択された特定のタイプに関係なく、これらのタイプをすべて使用できます。
存在型は不透明型です。
Unixのファイルハンドルを考えてください。その型はintであることがわかっているので、簡単に偽造できます。たとえば、ハンドル43から読み取ろうとすることができます。プログラムがこの特定のハンドルで開かれたファイルを持っている場合は、読み取ります。コードは悪意がある必要はなく、単にずさんなだけです(たとえば、ハンドルが初期化されていない変数である可能性があります)。
存在タイプはプログラムから隠されています。 fopen
が存在タイプを返した場合、この存在タイプを受け入れるいくつかのライブラリ関数でそれを使用するだけです。たとえば、次の擬似コードはコンパイルされます。
let exfile = fopen("foo.txt"); // No type for exfile!
read(exfile, buf, size);
インターフェイス「読み取り」は次のように宣言されます。
次のようなタイプTが存在します。
size_t read(T exfile, char* buf, size_t size);
変数exfileはintではなく、char*
ではなく、構造体Fileではありません。型システムでは表現できません。型が不明な変数を宣言することはできません。また、たとえば、その未知の型にポインターをキャストすることはできません。言語はあなたをさせません。
質問に直接回答するには:
ユニバーサルタイプでは、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
のクラスは普遍的です。 MyClass
のインスタンスを使用するときはいつでもTの実際の型を知っているMyClass
自体のインスタンスメソッドの観点からは、T
の実際の型がわからないため、T
は存在します。?
_は存在タイプを表します。したがって、クラス内にいるとき、T
は基本的に_?
_です。 MyClass
が存在するT
のインスタンスを処理する場合は、上記のsecretMessage()
の例のように_MyClass<?>
_を宣言できます。他の場所で説明したように、実在型は何かの実装の詳細を隠すために時々使用されます。 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に変換できます(または、データ構造)オンデマンド。
クロージャーを使用せずに、実在型のレコードをほとんど常にレコードに変換できます。クロージャーも実在的に型付けされます。クロージャーが閉じられた自由変数は呼び出し元から隠されます。したがって、存在型ではなくクロージャをサポートする言語を使用すると、オブジェクトの存在部分に入れたのと同じ隠し状態を共有するクロージャを作成できます。
私は少し遅れているようですが、とにかく、このドキュメントは存在型が何であるかの別のビューを追加します、具体的に言語に依存しないが、それから存在型を理解することはかなり簡単になるはずです: http:// www .cs.uu.nl/groups/ST/Projects/ehc/ehc-book.pdf (第8章)
普遍的と実存的に数量化されたタイプの違いは、次の観察によって特徴付けられます。
数量化されたタイプvalueを持つ値の使用により、数量化されたタイプ変数のインスタンス化のために選択するタイプが決まります。たとえば、ID関数「id ::∀a.a→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は、参照されることはないため、サブタイプで定義する必要のない抽象タイプです。これは事実上、簡略表記法です。 JavaのList[_]
Scalaで およびList<?>
の。
私の答えは、Martin Oderskyの 統一する提案 抽象型と実存型に触発されました。 付随するスライド は理解を助けます。
抽象データ型と情報隠蔽の研究により、実存型がプログラミング言語にもたらされました。データ型を抽象化すると、その型に関する情報が非表示になるため、その型のクライアントはそれを悪用できません。オブジェクトへの参照があるとしましょう。一部の言語では、その参照をバイトへの参照にキャストし、そのメモリに必要な処理を実行できます。プログラムの動作を保証するためには、オブジェクトのデザイナーが提供するメソッドを介してのみオブジェクトへの参照に基づいて行動するように強制することは、言語にとって有用です。タイプは存在しますが、それ以上はありません。
見る:
抽象型には実在型、MITCHELおよびPLOTKINがあります