GADTs bieten die klare und bessere Syntax für Code mithilfe von Existenztypen, indem implizite Foralls bereitgestellt werden
Ich denke, es besteht allgemeine Übereinstimmung darüber, dass die GADT-Syntax besser ist. Ich würde nicht sagen, dass dies daran liegt, dass GADTs implizite Foralls bereitstellen, sondern dass die ursprüngliche Syntax, die mit der ExistentialQuantification
Erweiterung aktiviert wurde , möglicherweise verwirrend / irreführend ist. Diese Syntax sieht natürlich so aus:
data SomeType = forall a. SomeType a
oder mit einer Einschränkung:
data SomeShowableType = forall a. Show a => SomeShowableType a
und ich denke, der Konsens ist, dass die Verwendung des Schlüsselworts forall
hier ermöglicht, dass der Typ leicht mit dem völlig anderen Typ verwechselt werden kann:
data AnyType = AnyType (forall a. a) -- need RankNTypes extension
Bei einer besseren Syntax wurde möglicherweise ein separates exists
Schlüsselwort verwendet, sodass Sie Folgendes schreiben würden:
data SomeType = SomeType (exists a. a) -- not valid GHC syntax
Die implizite oder explizite GADT-Syntax forall
ist für diese Typen einheitlicher und scheint leichter zu verstehen. Selbst mit einer expliziten forall
Definition vermittelt die folgende Definition die Idee, dass Sie einen Wert eines beliebigen Typs a
in einen monomorphen Wert einfügen können SomeType'
:
data SomeType' where
SomeType' :: forall a. (a -> SomeType') -- parentheses optional
und es ist leicht, den Unterschied zwischen diesem Typ zu erkennen und zu verstehen:
data AnyType' where
AnyType' :: (forall a. a) -> AnyType'
Existenzielle Typen scheinen nicht an dem Typ interessiert zu sein, den sie enthalten, aber Mustervergleiche besagen, dass es einen Typ gibt, von dem wir nicht wissen, um welchen Typ es sich handelt, bis wir Typeable oder Data verwenden.
Wir verwenden sie, wenn wir Typen ausblenden möchten (z. B. für heterogene Listen) oder wenn wir nicht wirklich wissen, welche Typen zur Kompilierungszeit vorhanden sind.
Ich denke, diese sind nicht zu weit entfernt, obwohl Sie keine existenziellen Typen verwenden Typeable
oder Data
verwenden müssen. Ich denke, es wäre genauer zu sagen, dass ein existenzieller Typ eine gut typisierte "Box" um einen nicht spezifizierten Typ liefert. Das Feld "versteckt" den Typ in gewissem Sinne, wodurch Sie eine heterogene Liste solcher Felder erstellen können, wobei die darin enthaltenen Typen ignoriert werden. Es stellt sich heraus, dass ein nicht eingeschränktes Existenzial wie SomeType'
oben ziemlich nutzlos ist, aber ein eingeschränkter Typ:
data SomeShowableType' where
SomeShowableType' :: forall a. (Show a) => a -> SomeShowableType'
ermöglicht es Ihnen, Musterübereinstimmungen vorzunehmen, um einen Blick in die "Box" zu werfen und die Typklasseneinrichtungen verfügbar zu machen:
showIt :: SomeShowableType' -> String
showIt (SomeShowableType' x) = show x
Beachten Sie, dass dies für jede Typklasse funktioniert, nicht nur für Typeable
oder Data
.
In Bezug auf Ihre Verwirrung über Seite 20 des Dia-Decks sagt der Autor, dass es für eine existenzielle Funktion unmöglich ist , eine bestimmte Instanz Worker
zu fordern . Sie können eine Funktion schreiben, um eine mit einem bestimmten Typ von zu erstellen , wie z .Worker
Buffer
Worker
Buffer
MemoryBuffer
class Buffer b where
output :: String -> b -> IO ()
data Worker x = forall b. Buffer b => Worker {buffer :: b, input :: x}
data MemoryBuffer = MemoryBuffer
instance Buffer MemoryBuffer
memoryWorker = Worker MemoryBuffer (1 :: Int)
memoryWorker :: Worker Int
Wenn Sie jedoch eine Funktion schreiben, die ein Worker
as-Argument verwendet, kann sie nur die allgemeinen Buffer
Typklassenfunktionen (z. B. die Funktion output
) verwenden:
doWork :: Worker Int -> IO ()
doWork (Worker b x) = output (show x) b
Es kann nicht versucht werden, zu verlangen, dass es sich b
um einen bestimmten Puffertyp handelt, auch nicht durch Mustervergleich:
doWorkBroken :: Worker Int -> IO ()
doWorkBroken (Worker b x) = case b of
MemoryBuffer -> error "try this" -- type error
_ -> error "try that"
Schließlich werden Laufzeitinformationen zu existenziellen Typen durch implizite "Wörterbuch" -Argumente für die beteiligten Typklassen verfügbar gemacht. Der Worker
obige Typ hat zusätzlich zu den Feldern für den Puffer und die Eingabe auch ein unsichtbares implizites Feld, das auf das Buffer
Wörterbuch verweist (ähnlich wie die V-Tabelle, obwohl sie kaum riesig ist, da sie nur einen Zeiger auf die entsprechende output
Funktion enthält).
Intern wird die Typklasse Buffer
als Datentyp mit Funktionsfeldern dargestellt, und Instanzen sind "Wörterbücher" dieses Typs:
data Buffer' b = Buffer' { output' :: String -> b -> IO () }
dBuffer_MemoryBuffer :: Buffer' MemoryBuffer
dBuffer_MemoryBuffer = Buffer' { output' = undefined }
Der existentielle Typ hat ein verstecktes Feld für dieses Wörterbuch:
data Worker' x = forall b. Worker' { dBuffer :: Buffer' b, buffer' :: b, input' :: x }
und eine solche Funktion doWork
, die mit existenziellen Worker'
Werten arbeitet, wird implementiert als:
doWork' :: Worker' Int -> IO ()
doWork' (Worker' dBuf b x) = output' dBuf (show x) b
Für eine Typklasse mit nur einer Funktion ist das Wörterbuch tatsächlich auf einen neuen Worker
Typ optimiert. In diesem Beispiel enthält der existenzielle Typ ein verstecktes Feld, das aus einem Funktionszeiger auf die output
Funktion für den Puffer besteht. Dies ist die einzige erforderliche Laufzeitinformation von doWork
.
AnyType
Rang-2-Typ nennen sollen; Das ist nur verwirrend und ich habe es gelöscht. Der KonstruktorAnyType
verhält sich wie eine Rang-2-Funktion, und der Konstruktor verhält sichSomeType
wie eine Rang-1-Funktion (genau wie die meisten nicht- existentiellen Typen), aber das ist keine sehr hilfreiche Charakterisierung. Wenn überhaupt, ist das Interessante an diesen Typen, dass sie selbst den Rang 0 haben (dh nicht über eine Typvariable quantifiziert und somit monomorph sind), obwohl sie quantifizierte Typen "enthalten".Da
Worker
wie definiert nur ein Argument verwendet wird, der Typ des "Eingabe" -Felds (Typvariablex
). ZBWorker Int
ist ein Typ. Die Typvariableb
ist stattdessen kein Parameter vonWorker
, sondern sozusagen eine Art "lokale Variable". Es kann nicht wie in übergeben werdenWorker Int String
- das würde einen Typfehler auslösen.Wenn wir stattdessen definieren:
dann
Worker Int String
würde es funktionieren, aber der Typ ist nicht mehr existenziell - wir müssen jetzt immer auch den Puffertyp übergeben.Das ist ungefähr richtig. Kurz gesagt, jedes Mal, wenn Sie den Konstruktor anwenden
Worker
, leitet GHC denb
Typ aus den Argumenten von abWorker
und sucht dann nach einer InstanzBuffer b
. Wenn dies gefunden wird, enthält GHC einen zusätzlichen Zeiger auf die Instanz im Objekt. In seiner einfachsten Form unterscheidet sich dies nicht allzu sehr von dem "Zeiger auf vtable", der jedem Objekt in OOP hinzugefügt wird, wenn virtuelle Funktionen vorhanden sind.Im allgemeinen Fall kann es jedoch viel komplexer sein. Der Compiler verwendet möglicherweise eine andere Darstellung und fügt anstelle eines einzelnen Zeigers weitere Zeiger hinzu (z. B. direktes Hinzufügen der Zeiger zu allen Instanzmethoden), wenn dies den Code beschleunigt. Manchmal muss der Compiler auch mehrere Instanzen verwenden, um eine Einschränkung zu erfüllen. Wenn wir beispielsweise die Instanz für
Eq [Int]
... speichern müssen, gibt es nicht eine, sondern zwei: eine fürInt
und eine für Listen, und die beiden müssen kombiniert werden (zur Laufzeit, außer bei Optimierungen).Es ist schwer zu erraten, was GHC in jedem Fall genau tut: Das hängt von einer Menge Optimierungen ab, die möglicherweise ausgelöst werden oder nicht.
Sie können versuchen, nach der "wörterbuchbasierten" Implementierung von Typklassen zu googeln, um mehr über die Vorgänge zu erfahren. Sie können GHC auch bitten, den internen optimierten Core mit
-ddump-simpl
den Wörterbüchern zu drucken und zu beobachten, die erstellt, gespeichert und weitergegeben werden. Ich muss Sie warnen: Der Kern ist eher niedrig und kann zunächst schwer zu lesen sein.quelle