Ich habe den Wikipedia-Artikel Existenzielle Typen gelesen . Ich habe festgestellt, dass sie aufgrund des existenziellen Operators (∃) als existenzielle Typen bezeichnet werden. Ich bin mir jedoch nicht sicher, worum es geht. Was ist der Unterschied zwischen
T = ∃X { X a; int f(X); }
und
T = ∀x { X a; int f(X); }
?
Antworten:
Wenn jemand einen universellen Typ definiert,
∀X
sagt er: Sie können jeden gewünschten Typ anschließen. Ich muss nichts über den Typ wissen, um meine Arbeit zu erledigen. Ich werde ihn nur undurchsichtig als bezeichnenX
.Wenn jemand einen existenziellen Typ definiert,
∃X
sagt er: Ich werde hier jeden Typ verwenden, den ich will; Sie wissen nichts über den Typ, daher können Sie ihn nur undurchsichtig als bezeichnenX
.Mit universellen Typen können Sie Dinge schreiben wie:
Die
copy
Funktion hat keine Ahnung, wasT
tatsächlich sein wird, muss es aber nicht.Mit existentiellen Typen können Sie Dinge schreiben wie:
Jede Implementierung einer virtuellen Maschine in der Liste kann einen anderen Bytecodetyp haben. Die
runAllCompilers
Funktion hat keine Ahnung, was der Bytecode-Typ ist, muss es aber nicht. Alles, was es tut, ist den Bytecode vonVirtualMachine.compile
bis weiterzuleitenVirtualMachine.run
.Java-Platzhalter (z. B. :)
List<?>
sind eine sehr begrenzte Form existenzieller Typen.Update: Ich habe vergessen zu erwähnen, dass Sie existenzielle Typen mit universellen Typen simulieren können. Wickeln Sie zuerst Ihren universellen Typ ein, um den Typparameter auszublenden. Zweitens: Invertieren Sie die Kontrolle (dies vertauscht effektiv den Teil "Sie" und "Ich" in den obigen Definitionen, was den Hauptunterschied zwischen Existentialen und Universalien darstellt).
Jetzt können wir
VMWrapper
unseren eigenen Aufruf haben, derVMHandler
eine universell typisiertehandle
Funktion hat. Der Nettoeffekt ist der gleiche, unser Code mussB
als undurchsichtig behandelt werden.Ein Beispiel für eine VM-Implementierung:
quelle
List<∃B:VirtualMachine<B>> vms
oder nicht vollständigfor (∃B:VirtualMachine<B> vm : vms)
. (Da es sich um generische Typen handelt, hätten Sie nicht Javas?
Platzhalter anstelle der "selbst erstellten" Syntax verwenden können?) Ich denke, es könnte hilfreich sein, ein Codebeispiel zu haben, bei dem keine generischen Typen∃B:VirtualMachine<B>
beteiligt sind, sondern ein "Straight".∃B
, weil generische Typen nach Ihren ersten Codebeispielen leicht mit universellen Typen verknüpft werden können.∃B
habe ich explizit angegeben, wo die Quantifizierung stattfindet. Bei der Platzhalter-Syntax wird der Quantifizierer impliziert (List<List<?>>
bedeutet eigentlich∃T:List<List<T>>
und nichtList<∃T:List<T>>
). Durch explizite Quantifizierung können Sie sich auch auf den Typ beziehen (ich habe das Beispiel geändert, um dies auszunutzen, indem ich den Bytecode des Typs speichereB
in einer temporären Variablen ).Ein Wert eines existenziellen Typs wie
∃x. F(x)
ist ein Paar, das einen Typx
und a enthält Wert des Typs enthältF(x)
. Während ein Wert einer polymorphen Typ wie∀x. F(x)
eine Funktion , die irgendeine Art nimmtx
und erzeugt einen Wert des TypsF(x)
. In beiden Fällen wird der Typ über einen Typkonstruktor geschlossenF
.Beachten Sie, dass in dieser Ansicht Typen und Werte gemischt werden. Der existenzielle Beweis ist ein Typ und ein Wert. Der universelle Beweis ist eine ganze Familie von Werten, die nach Typ indiziert sind (oder eine Zuordnung von Typen zu Werten).
Der Unterschied zwischen den beiden von Ihnen angegebenen Typen ist also wie folgt:
Dies bedeutet: Ein Wert vom Typ
T
enthält einen aufgerufenen TypX
, einen Werta:X
und eine Funktionf:X->int
. Ein Produzent vonT
Typwerten kann einen beliebigen Typ auswählen,X
und ein Verbraucher kann nichts darüber wissenX
. Nur dass es ein Beispiel dafür gibta
und dass dieser Wert in einen Wert umgewandelt werden kann,int
indem man ihn gibtf
. Mit anderen Worten, ein Wert vom TypT
weiß, wie manint
irgendwie einen erzeugt. Nun, wir könnten den Zwischentyp eliminierenX
und einfach sagen:Das universell quantifizierte ist etwas anders.
Dies bedeutet: Ein Wert vom Typ
T
kann einem beliebigen TypX
zugewiesen werden und erzeugt einen Werta:X
und eine Funktion,f:X->int
egal wasX
ist . Mit anderen Worten: Ein Verbraucher von TypwertenT
kann einen beliebigen Typ für auswählenX
. Und ein Produzent von TypwertenT
kann überhaupt nichts wissenX
, aber er muss in der Lage sein, einen Werta
für jede Wahl zu produzieren und einenX
solchen Wert in einen zu verwandelnint
.Offensichtlich ist die Implementierung dieses Typs unmöglich, da es kein Programm gibt, das einen Wert für jeden erdenklichen Typ erzeugen kann. Es sei denn, Sie erlauben Absurditäten wie
null
oder Bottoms.Da ein Existenzial ein Paar ist, kann ein existenzielles Argument durch Currying in ein universelles umgewandelt werden .
ist das gleiche wie:
Ersteres ist ein existenzieller Rang 2 . Dies führt zu der folgenden nützlichen Eigenschaft:
Es gibt einen Standardalgorithmus zur Umwandlung von Existentialen in Universalien namens Skolemisierung .
quelle
Ich denke, es ist sinnvoll, existenzielle Typen zusammen mit universellen Typen zu erklären, da die beiden Konzepte komplementär sind, dh das eine ist das "Gegenteil" des anderen.
Ich kann nicht jedes Detail über existenzielle Typen beantworten (z. B. eine genaue Definition geben, alle möglichen Verwendungen auflisten, ihre Beziehung zu abstrakten Datentypen usw.), weil ich dafür einfach nicht gut genug informiert bin. Ich werde nur (unter Verwendung von Java) zeigen, was dieser HaskellWiki-Artikel als Haupteffekt existenzieller Typen bezeichnet:
Beispielaufbau:
Der folgende Pseudocode ist nicht ganz gültiges Java, obwohl es einfach genug wäre, dies zu beheben. Genau das werde ich in dieser Antwort tun!
Lassen Sie mich dies kurz für Sie darlegen. Wir definieren ...
Ein rekursiver Typ,
Tree<α>
der einen Knoten in einem Binärbaum darstellt. Jeder Knoten speichert einenvalue
Typ α und verweist auf optionaleleft
undright
Teilbäume desselben Typs.Eine Funktion,
height
die den weitesten Abstand von einem Blattknoten zum Wurzelknoten zurückgibtt
.Lassen Sie uns nun den obigen Pseudocode für
height
in die richtige Java-Syntax umwandeln ! (Ich werde der Kürze halber weiterhin einige Boilerplate weglassen, z. B. Modifikatoren für Objektorientierung und Barrierefreiheit.) Ich werde zwei mögliche Lösungen zeigen.1. Universelle Lösung:
Die naheliegendste Lösung besteht darin, einfach
height
generisch zu machen, indem der Typparameter α in seine Signatur eingefügt wird:Auf diese Weise können Sie Variablen deklarieren und innerhalb dieser Funktion Ausdrücke vom Typ α erstellen , wenn Sie dies möchten . Aber...
2. Existenzielle Lösung:
Wenn Sie sich den Körper unserer Methode ansehen, werden Sie feststellen, dass wir nicht auf etwas vom Typ α zugreifen oder damit arbeiten ! Es gibt keine Ausdrücke mit diesem Typ oder Variablen, die mit diesem Typ deklariert wurden. Warum müssen wir also überhaupt
height
generisch machen ? Warum können wir α nicht einfach vergessen ? Wie sich herausstellt, können wir:Wie ich ganz am Anfang dieser Antwort schrieb, sind existenzielle und universelle Typen komplementärer / dualer Natur. Wenn also die Universallösung zu machen ist
height
mehr generisch, dann sollten wir erwarten , dass existentielle Typen die entgegengesetzte Wirkung haben: so dass es weniger generisch, nämlich durch Verstecken / Entfernen der Typ - Parameter α .Infolgedessen können Sie
t.value
in dieser Methode nicht mehr auf den Typ von verweisen oder Ausdrücke dieses Typs bearbeiten, da kein Bezeichner daran gebunden wurde. (Der?
Platzhalter ist ein spezielles Token, kein Bezeichner, der einen Typ "erfasst".)t.value
Ist effektiv undurchsichtig geworden. Vielleicht ist das einzige, was Sie noch damit machen können, eine TypumwandlungObject
.Zusammenfassung:
quelle
Object
ziemlich interessant: Während beide insofern ähnlich sind, als Sie statisch typunabhängigen Code schreiben können, werfen die ersteren (Generika) nicht einfach fast alle verfügbaren Typinformationen weg erreiche dieses Ziel. In diesem speziellen Sinne sind Generika ein Mittel gegenObject
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
ist einuniversal type
und?
stellt einexistential type
??
in dem Typint height(Tree<?> t)
ist innerhalb der Funktion immer noch nicht bekannt und wird immer noch vom Aufrufer bestimmt, da der Aufrufer auswählen muss, welcher Baum übergeben werden soll. Auch wenn Leute dies den existenziellen Typ in Java nennen, ist dies nicht der Fall. Der?
Platzhalter kann unter bestimmten Umständen verwendet werden, um eine Form von Existentials in Java zu implementieren. Dies ist jedoch keine davon.Dies sind alles gute Beispiele, aber ich entscheide mich, sie etwas anders zu beantworten. Erinnern Sie sich an Mathe, dass ∀x. P (x) bedeutet "für alle x kann ich beweisen, dass P (x)". Mit anderen Worten, es ist eine Art Funktion, Sie geben mir ein x und ich habe eine Methode, um es für Sie zu beweisen.
In der Typentheorie sprechen wir nicht von Beweisen, sondern von Typen. In diesem Bereich meinen wir also "für jeden Typ X, den Sie mir geben, gebe ich Ihnen einen bestimmten Typ P". Da wir P nicht viele Informationen über X geben, außer dass es sich um einen Typ handelt, kann P nicht viel damit anfangen, aber es gibt einige Beispiele. P kann den Typ "alle Paare des gleichen Typs" erstellen :
P<X> = Pair<X, X> = (X, X)
. Oder wir können den Optionstyp erstellen:P<X> = Option<X> = X | Nil
wobei Nil der Typ der Nullzeiger ist. Wir können daraus eine Liste machen :List<X> = (X, List<X>) | Nil
. Beachten Sie, dass der letzte rekursiv ist. Die Werte vonList<X>
sind entweder Paare, bei denen das erste Element ein X und das zweite Element ein X istList<X>
oder es ist ein Nullzeiger.Nun, in Mathe ∃x. P (x) bedeutet "Ich kann beweisen, dass es ein bestimmtes x gibt, so dass P (x) wahr ist". Es mag viele solcher x geben, aber um es zu beweisen, reicht eines aus. Eine andere Möglichkeit ist, dass es eine nicht leere Menge von Beweis-und-Beweis-Paaren {(x, P (x))} geben muss.
Übersetzt in die Typentheorie: Ein Typ in der Familie
∃X.P<X>
ist ein Typ X und ein entsprechender TypP<X>
. Beachten Sie, dass, bevor wir P X gegeben haben (so dass wir alles über X außer P sehr wenig wussten), dass jetzt das Gegenteil der Fall ist.P<X>
verspricht nicht, irgendwelche Informationen über X zu geben, nur dass es eine gibt und dass es sich tatsächlich um einen Typ handelt.Wie ist das nützlich? Nun, P könnte ein Typ sein, der seinen internen Typ X offenlegen kann. Ein Beispiel wäre ein Objekt, das die interne Darstellung seines Zustands X verbirgt. Obwohl wir keine Möglichkeit haben, ihn direkt zu manipulieren, können wir seine Wirkung durch beobachten Es könnte viele Implementierungen dieses Typs geben, aber Sie könnten alle diese Typen verwenden, unabhängig davon, welcher ausgewählt wurde.
quelle
P<X>
statt einer handeltP
(gleiche Funktionalität und Containertyp, sagen wir, aber Sie wissen nicht, dass sie enthältX
)?∀x. P(x)
bedeutet nichts über die Beweisbarkeit vonP(x)
, nur die Wahrheit.So beantworten Sie Ihre Frage direkt:
Beim universellen Typ muss die Verwendung von
T
den Typparameter enthaltenX
. Zum BeispielT<String>
oderT<Integer>
. Für die existenziellen Typverwendungen vonT
enthalten Sie diesen Typparameter nicht, da er unbekannt oder irrelevant ist - verwenden Sie ihn einfachT
(oder in Java)T<?>
).Weitere Informationen:
Universelle / abstrakte Typen und existenzielle Typen sind eine Dualität der Perspektive zwischen dem Verbraucher / Kunden eines Objekts / einer Funktion und dem Produzenten / der Implementierung desselben. Wenn eine Seite einen universellen Typ sieht, sieht die andere einen existenziellen Typ.
In Java können Sie eine generische Klasse definieren:
MyClass
,T
ist universell, weil Sie jeden Typ ersetzen könnenT
, wenn Sie diese Klasse verwenden , und Sie müssen die tatsächliche Art von T wissen , wann immer Sie eine Instanz verwendenMyClass
MyClass
sichT
ist es existenziell, weil es den wirklichen Typ von nicht kenntT
?
den existenziellen Typ dar - wenn Sie sich also innerhalb der Klasse befinden,T
ist dies im Grunde genommen der Fall?
. Wenn Sie eine Instanz vonMyClass
mitT
existential behandeln möchten , können SieMyClass<?>
wie imsecretMessage()
obigen Beispiel deklarieren .Existenzielle Typen werden manchmal verwendet, um die Implementierungsdetails von etwas zu verbergen, wie an anderer Stelle erläutert. Eine Java-Version davon könnte folgendermaßen aussehen:
Es ist etwas schwierig, dies richtig zu erfassen, da ich vorgebe, in einer funktionalen Programmiersprache zu sein, die Java nicht ist. Der Punkt hier ist jedoch, dass Sie eine Art von Status sowie eine Liste von Funktionen erfassen, die in diesem Status ausgeführt werden, und den tatsächlichen Typ des Status-Teils nicht kennen, die Funktionen jedoch, da sie bereits mit diesem Typ abgeglichen wurden .
In Java sind nun alle nicht endgültigen nicht-primitiven Typen teilweise existenziell. Dies mag seltsam klingen, aber weil eine Variable, die als deklariert wurde,
Object
möglicherweise eine Unterklasse von sein könnteObject
deklariert wurde, stattdessen nicht den spezifischen Typ deklarieren kann, sondern nur "diesen Typ oder eine Unterklasse". Objekte werden also als Statusbit plus eine Liste von Funktionen dargestellt, die mit diesem Status arbeiten. Welche Funktion aufgerufen werden soll, wird zur Laufzeit durch Nachschlagen bestimmt. Dies ähnelt stark der Verwendung von existentiellen Typen, bei denen Sie einen existenziellen Zustandsteil und eine Funktion haben, die mit diesem Zustand arbeitet.In statisch typisierten Programmiersprachen ohne Subtypisierung und Casts ermöglichen existenzielle Typen die Verwaltung von Listen unterschiedlich typisierter Objekte. Eine Liste von
T<Int>
darf keine enthaltenT<Long>
. Eine Liste vonT<?>
kann jedoch eine beliebige Variation von enthaltenT
, sodass viele verschiedene Datentypen in die Liste aufgenommen und bei Bedarf alle in ein int konvertiert werden können (oder alle in der Datenstruktur bereitgestellten Vorgänge ausgeführt werden können).Man kann so ziemlich immer einen Datensatz mit einem existenziellen Typ in einen Datensatz konvertieren, ohne Verschlüsse zu verwenden. Ein Abschluss wird auch existenziell typisiert, indem die freien Variablen, über die er geschlossen wird, vor dem Aufrufer verborgen sind. Mit einer Sprache, die Verschlüsse, aber keine existenziellen Typen unterstützt, können Sie daher Verschlüsse erstellen, die denselben verborgenen Zustand aufweisen, den Sie in den existenziellen Teil eines Objekts eingefügt hätten.
quelle
Ein existenzieller Typ ist ein undurchsichtiger Typ.
Stellen Sie sich ein Dateihandle in Unix vor. Sie wissen, dass der Typ int ist, sodass Sie ihn leicht fälschen können. Sie können beispielsweise versuchen, aus Handle 43 zu lesen. Wenn das Programm eine Datei mit diesem bestimmten Handle geöffnet hat, lesen Sie daraus. Ihr Code muss nicht böswillig sein, sondern nur schlampig (z. B. könnte das Handle eine nicht initialisierte Variable sein).
Ein existenzieller Typ ist in Ihrem Programm verborgen. Wenn
fopen
ein existenzieller Typ zurückgegeben wird, können Sie ihn nur mit einigen Bibliotheksfunktionen verwenden, die diesen existenziellen Typ akzeptieren. Zum Beispiel würde der folgende Pseudocode kompiliert:Die Schnittstelle "read" wird deklariert als:
Es gibt einen Typ T, so dass:
Die Variable exfile ist keine int-, keine
char*
, keine struct-Datei - nichts, was Sie im Typsystem ausdrücken können. Sie können keine Variable deklarieren, deren Typ unbekannt ist, und Sie können beispielsweise keinen Zeiger in diesen unbekannten Typ umwandeln. Die Sprache lässt dich nicht.quelle
read
lautet∃T.read(T file, ...)
, können Sie nichts als ersten Parameter übergeben. Was funktionieren würde, wäre,fopen
das Dateihandle und eine Lesefunktion zurückzugeben, die von demselben Existenzial erfasst werden :∃T.(T, read(T file, ...))
Scheint, als würde ich etwas spät kommen, aber trotzdem fügt dieses Dokument eine andere Ansicht der existenziellen Typen hinzu, obwohl es nicht spezifisch sprachunabhängig ist , sollte es dann ziemlich einfacher sein, existenzielle Typen zu verstehen: http: //www.cs.uu .nl / groups / ST / Projects / ehc / ehc-book.pdf (Kapitel 8)
quelle
Für alle Werte der Typparameter existiert ein universeller Typ. Ein existentieller Typ existiert nur für Werte der Typparameter, die die Einschränkungen des existentiellen Typs erfüllen.
In Scala ist beispielsweise eine Möglichkeit, einen existenziellen Typ auszudrücken, ein abstrakter Typ, der auf einige obere oder untere Grenzen beschränkt ist.
Entsprechend ist ein eingeschränkter Universaltyp ein existenzieller Typ wie im folgenden Beispiel.
Jede Verwendungssite kann das verwenden,
Interface
da alle instanziierbaren Untertypen vonExistential
definieren müssen,type Parameter
welche das implementieren müssenInterface
.Ein entarteter Fall eines existenziellen Typs in Scala ist ein abstrakter Typ, auf den niemals Bezug genommen wird und der daher von keinem Subtyp definiert werden muss. Dies hat effektiv eine Kurzschreibweise von
List[_]
in Scala undList<?>
in Java.Meine Antwort wurde von Martin Oderskys Vorschlag inspiriert , abstrakte und existenzielle Typen zu vereinen . Die beiliegende Folie hilft beim Verständnis.
quelle
∀x.f(x)
sind für ihre Empfangsfunktionen undurchsichtig, während Existenzielle Typen∃x.f(x)
auf bestimmte Eigenschaften beschränkt sind. In der Regel sind alle Parameter existenziell, da sie von ihrer Funktion direkt bearbeitet werden. Generische Parameter können jedoch universelle Typen haben, da die Funktion sie nicht über grundlegende universelle Operationen hinaus verwaltet, z. B. das Erhalten einer Referenz wie in:∀x.∃array.copy(src:array[x] dst:array[x]){...}
forSome
für den Typparameter eine existenzielle Quantifizierung.Die Erforschung abstrakter Datentypen und das Verstecken von Informationen brachten existenzielle Typen in Programmiersprachen. Wenn Sie einen Datentyp abstrakt erstellen, werden Informationen zu diesem Typ ausgeblendet, sodass ein Client dieses Typs ihn nicht missbrauchen kann. Angenommen, Sie haben einen Verweis auf ein Objekt. In einigen Sprachen können Sie diesen Verweis auf einen Verweis auf Bytes umwandeln und mit diesem Speicherelement alles tun, was Sie möchten. Um das Verhalten eines Programms zu gewährleisten, ist es für eine Sprache hilfreich, zu erzwingen, dass Sie nur über die vom Designer des Objekts bereitgestellten Methoden auf den Verweis auf das Objekt reagieren. Sie wissen, dass der Typ existiert, aber nichts weiter.
quelle
Ich habe dieses Diagramm erstellt. Ich weiß nicht, ob es streng ist. Aber wenn es hilft, bin ich froh.
quelle
Soweit ich weiß, ist es eine mathematische Art, Schnittstellen / abstrakte Klassen zu beschreiben.
Wie für T = ∃X {X a; int f (X); }}
Für C # würde es in einen generischen abstrakten Typ übersetzt:
"Existenziell" bedeutet nur, dass es einen Typ gibt, der den hier definierten Regeln entspricht.
quelle