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
.