Ich beherrsche Haskell nicht wirklich, daher könnte dies eine sehr einfache Frage sein.
Welche Sprachbeschränkung lösen Rank2Types ? Unterstützen Funktionen in Haskell nicht bereits polymorphe Argumente?
haskell
types
polymorphism
higher-rank-types
Andrey Shchekin
quelle
quelle
Antworten:
Sie tun dies, aber nur von Rang 1. Dies bedeutet, dass Sie zwar eine Funktion schreiben können, die verschiedene Arten von Argumenten ohne diese Erweiterung akzeptiert, Sie jedoch keine Funktion schreiben können, die ihr Argument als verschiedene Typen in demselben Aufruf verwendet.
Beispielsweise kann die folgende Funktion ohne diese Erweiterung nicht eingegeben werden, da
g
sie bei der Definition von mit verschiedenen Argumenttypen verwendet wirdf
:Beachten Sie, dass es durchaus möglich ist, eine polymorphe Funktion als Argument an eine andere Funktion zu übergeben. So etwas
map id ["a","b","c"]
ist also völlig legal. Die Funktion kann sie jedoch nur als monomorph verwenden. Im Beispiel wirdmap
verwendet,id
als ob es Typ hätteString -> String
. Und natürlich können Sie statt auch eine einfache monomorphe Funktion des angegebenen Typs übergebenid
. Ohne rank2types gibt es keine Möglichkeit für eine Funktion, zu verlangen, dass ihr Argument eine polymorphe Funktion sein muss, und daher auch keine Möglichkeit, sie als polymorphe Funktion zu verwenden.quelle
f' g x y = g x + g y
. Sein abgeleiteter Rang-1-Typ istforall a r. Num r => (a -> r) -> a -> a -> r
. Daforall a
sich der Aufrufer außerhalb der Funktionspfeile befindet, muss er zuerst einen Typ für auswählena
. Wenn sie auswählenInt
, bekommen wirf' :: forall r. Num r => (Int -> r) -> Int -> Int -> r
, und jetzt haben wir dasg
Argument korrigiert, damit es dauern kann,Int
aber nichtString
. Wenn wir aktivierenRankNTypes
, können wirf'
mit Typ kommentierenforall b c r. Num r => (forall a. a -> r) -> b -> c -> r
. Kann es aber nicht benutzen - was wäreg
das?Es ist schwer, einen höherrangigen Polymorphismus zu verstehen, wenn Sie System F nicht direkt studieren , da Haskell die Details davon im Interesse der Einfachheit vor Ihnen verbergen soll.
Aber im Grunde ist die grobe Idee, dass polymorphe Typen nicht wirklich die
a -> b
Form haben, die sie in Haskell haben; In Wirklichkeit sehen sie so aus, immer mit expliziten Quantifizierern:Wenn Sie das Symbol "∀" nicht kennen, wird es als "für alle" gelesen.
∀x.dog(x)
bedeutet "für alle x ist x ein Hund." "Λ" ist das Großbuchstaben Lambda, das zum Abstrahieren über Typparameter verwendet wird. In der zweiten Zeile heißt es, dass id eine Funktion ist, die einen Typ annimmtt
und dann eine Funktion zurückgibt, die von diesem Typ parametrisiert wird.Sie sehen, in System F können Sie eine solche Funktion nicht einfach
id
sofort auf einen Wert anwenden . Zuerst müssen Sie die Λ-Funktion auf einen Typ anwenden, um eine λ-Funktion zu erhalten, die Sie auf einen Wert anwenden. Also zum Beispiel:Standard Haskell (dh Haskell 98 und 2010) vereinfacht dies für Sie, indem es keine dieser Typquantifizierer, Großbuchstaben und Typanwendungen hat, aber hinter den Kulissen setzt GHC sie ein, wenn es das Programm zur Kompilierung analysiert. (Ich glaube, das ist alles zur Kompilierungszeit, ohne Laufzeitaufwand.)
Haskells automatische Behandlung bedeutet jedoch, dass davon ausgegangen wird, dass "∀" niemals im linken Zweig eines Funktionstyps ("→") erscheint.
Rank2Types
undRankNTypes
schalten Sie diese Beschränkungen und ermöglichen es Ihnen Haskells Standardregeln für außer Kraft zu setzen , wo einfügenforall
.Warum willst du das tun? Weil das volle, uneingeschränkte System F hella mächtig ist und viele coole Sachen machen kann. Beispielsweise können das Ausblenden und die Modularität von Typen mithilfe von Typen mit höherem Rang implementiert werden. Nehmen Sie zum Beispiel eine einfache alte Funktion des folgenden Rang-1-Typs (um die Szene einzustellen):
Zur Verwendung
f
muss der Aufrufer zuerst auswählen, für welche Typen er verwendet werden soll,r
unda
dann ein Argument des resultierenden Typs angeben. So könnten Sie auswählenr = Int
unda = String
:Aber vergleichen Sie das jetzt mit dem folgenden höherrangigen Typ:
Wie funktioniert eine solche Funktion? Um es zu verwenden, geben Sie zuerst an, für welchen Typ Sie es verwenden möchten
r
. Sagen wir, wir wählenInt
:Aber jetzt
∀a
befindet sich der innerhalb des Funktionspfeils, sodass Sie nicht auswählen können, für welchen Typ Sie ihn verwenden möchtena
. Sie müssen sichf' Int
auf eine Λ-Funktion des entsprechenden Typs anwenden . Dies bedeutet, dass bei der Implementierung von ausgewähltf'
wird, für welchen Typ verwendet werden solla
, nicht für den Aufrufer vonf'
. Im Gegensatz dazu wählt der Anrufer ohne höherrangige Typen immer die Typen aus.Wofür ist das nützlich? Nun, für viele Dinge tatsächlich, aber eine Idee ist, dass Sie dies verwenden können, um Dinge wie objektorientierte Programmierung zu modellieren, bei der "Objekte" einige versteckte Daten zusammen mit einigen Methoden bündeln, die mit den versteckten Daten arbeiten. So könnte beispielsweise ein Objekt mit zwei Methoden - eine, die eine zurückgibt,
Int
und eine, die a zurückgibtString
- mit diesem Typ implementiert werden:Wie funktioniert das? Das Objekt wird als eine Funktion implementiert, die einige interne Daten vom versteckten Typ enthält
a
. Um das Objekt tatsächlich zu verwenden, übergeben seine Clients eine "Rückruffunktion", die das Objekt mit den beiden Methoden aufruft. Beispielsweise:Hier rufen wir im Grunde die zweite Methode des Objekts auf, die, deren Typ
a → String
für ein Unbekanntes ista
. Nun, denmyObject
Kunden unbekannt ; Diese Clients wissen jedoch anhand der Signatur, dass sie eine der beiden Funktionen darauf anwenden und entweder eineInt
oder eine erhalten könnenString
.Für ein aktuelles Haskell-Beispiel ist unten der Code aufgeführt, den ich geschrieben habe, als ich mich selbst unterrichtet habe
RankNTypes
. Dies implementiert einen aufgerufenen Typ,ShowBox
der einen Wert eines versteckten Typs zusammen mit seinerShow
Klasseninstanz bündelt . Beachten Sie, dass ich im Beispiel unten eine Liste erstelle,ShowBox
deren erstes Element aus einer Zahl und das zweite aus einer Zeichenfolge besteht. Da die Typen durch die Verwendung der höherrangigen Typen ausgeblendet werden, verstößt dies nicht gegen die Typprüfung.PS: Für jeden, der dies liest und sich gefragt hat, wie es dazu kommt, dass
ExistentialTypes
GHC verwendet wirdforall
, glaube ich, dass der Grund darin liegt, dass diese Art von Technik hinter den Kulissen verwendet wird.quelle
exists
Schlüsselwort hätten, könnten Sie einen existenziellen Typ als (zum Beispiel) definierendata Any = Any (exists a. a)
, wobeiAny :: (exists a. a) -> Any
. Mit ∀xP (x) → Q ≡ (∃xP (x)) → Q können wir schließen, dassAny
es auch einen Typ geben könnte,forall a. a -> Any
und daherforall
kommt das Schlüsselwort. Ich glaube, dass existentielle Typen, wie sie von GHC implementiert werden, nur gewöhnliche Datentypen sind, die auch alle erforderlichen Typklassenwörterbücher enthalten (ich konnte leider keinen Verweis finden, um dies zu sichern).data ApplyBox r = forall a. ApplyBox (a -> r) a
; Wenn Sie Musterübereinstimmung mit erhaltenApplyBox f x
, erhalten Sief :: h -> r
undx :: h
für einen "versteckten" eingeschränkten Typh
. Wenn ich richtig verstehe, wird der Typklassenwörterbuch-Fall in so etwasdata ShowBox = forall a. Show a => ShowBox a
übersetzt : wird in so etwas übersetztdata ShowBox' = forall a. ShowBox' (ShowDict' a) a
;instance Show ShowBox' where show (ShowBox' dict val) = show' dict val
;;show' :: ShowDict a -> a -> String
.Die Antwort von Luis Casillas gibt viele großartige Informationen darüber, was Rang-2-Typen bedeuten, aber ich werde nur auf einen Punkt eingehen, den er nicht behandelt hat. Wenn ein Argument polymorph sein muss, kann es nicht nur mit mehreren Typen verwendet werden. Es schränkt auch ein, was diese Funktion mit ihren Argumenten tun kann und wie sie ihr Ergebnis erzeugen kann. Das heißt, es gibt dem Anrufer weniger Flexibilität. Warum willst du das tun? Ich beginne mit einem einfachen Beispiel:
Angenommen, wir haben einen Datentyp
und wir wollen eine Funktion schreiben
Dies erfordert eine Funktion, die eines der Elemente der Liste auswählt und eine
IO
Aktion zurückgibt, mit der Raketen auf dieses Ziel abgefeuert werden. Wir könntenf
einen einfachen Typ geben:Das Problem ist, dass wir versehentlich rennen könnten
und dann wären wir in großen Schwierigkeiten! Geben
f
eines polymorphen Typs vom Rang 1hilft überhaupt nicht, weil wir den Typ wählen,
a
wenn wir anrufenf
, und wir spezialisieren ihn einfach daraufCountry
und verwenden unseren bösartigen\_ -> BestAlly
wieder. Die Lösung besteht darin, einen Typ vom Rang 2 zu verwenden:Jetzt muss die Funktion, die wir übergeben, polymorph sein, also
\_ -> BestAlly
keine Typprüfung! Tatsächlich wird keine Funktion, die ein Element zurückgibt, das nicht in der angegebenen Liste enthalten ist, typecheck (obwohl einige Funktionen, die in Endlosschleifen gehen oder Fehler erzeugen und daher niemals zurückkehren, dies tun).Das Obige ist natürlich erfunden, aber eine Variation dieser Technik ist der Schlüssel, um die
ST
Monade sicher zu machen.quelle
Höherrangige Typen sind nicht so exotisch wie die anderen Antworten. Ob Sie es glauben oder nicht, viele objektorientierte Sprachen (einschließlich Java und C #!) Enthalten sie. (Natürlich kennt sie niemand in diesen Gemeinden unter dem beängstigend klingenden Namen "höherrangige Typen".)
Das Beispiel, das ich geben werde, ist eine Lehrbuchimplementierung des Besuchermusters, die ich ständig in meiner täglichen Arbeit verwende. Diese Antwort ist nicht als Einführung in das Besuchermuster gedacht. Dieses Wissen ist an anderer Stelle leicht verfügbar .
In dieser fetten imaginären HR-Anwendung möchten wir Mitarbeiter bearbeiten, die Vollzeit-Festangestellte oder Zeitarbeitskräfte sein können. Meine bevorzugte Variante des Besuchermusters (und tatsächlich diejenige, die für relevant ist
RankNTypes
) parametrisiert den Rückgabetyp des Besuchers.Der Punkt ist, dass eine Reihe von Besuchern mit unterschiedlichen Rückgabetypen alle mit denselben Daten arbeiten können. Dies bedeutet
IEmployee
, dass keine Meinung darüberT
geäußert werden darf, was sein sollte.Ich möchte Ihre Aufmerksamkeit auf die Typen lenken. Beachten Sie, dass
IEmployeeVisitor
der Rückgabetyp universell quantifiziert wird, währendIEmployee
er innerhalb seinerAccept
Methode quantifiziert wird, dh auf einem höheren Rang. Klobig von C # nach Haskell übersetzen:Da haben Sie es also. Höherrangige Typen werden in C # angezeigt, wenn Sie Typen schreiben, die generische Methoden enthalten.
quelle
Folien aus Bryan O'Sullivans Haskell-Kurs in Stanford halfen mir zu verstehen
Rank2Types
.quelle
Für diejenigen, die mit objektorientierten Sprachen vertraut sind, ist eine höherrangige Funktion einfach eine generische Funktion, die als Argument eine andere generische Funktion erwartet.
ZB in TypeScript könnten Sie schreiben:
Sehen Sie, wie der generische Funktionstyp
Identify
eine generische Funktion des Typs erfordertIdentifier
? Dies machtIdentify
eine höherrangige Funktion.quelle
Accept
hat einen polymorphen Typ vom Rang 1, aber es ist eine Methode vonIEmployee
, die selbst Rang 2 ist. Wenn mir jemand eine gibtIEmployee
, kann ich sie öffnen undAccept
bei jedem Typ anwenden.Visitee
Klasse, die Sie einführen. Eine Funktionf :: Visitee e => T e
ist (sobald das Klassenmaterial deaktiviert ist) im Wesentlichenf :: (forall r. e -> Visitor e r -> r) -> T e
. Mit Haskell 2010 können Sie mit solchen Klassen mit eingeschränktem Rang-2-Polymorphismus davonkommen.forall
in meinem Beispiel nicht herausschweben . Ich habe keine Referenz zur Hand, aber vielleicht finden Sie etwas in "Scrap Your Type Classes" . Ein höherrangiger Polymorphismus kann zwar zu Problemen bei der Typprüfung führen, aber die im Klassensystem implizierte begrenzte Sortierung ist in Ordnung.