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 ExistentialQuantificationErweiterung 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 forallhier 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 existsSchlüsselwort verwendet, sodass Sie Folgendes schreiben würden:
data SomeType = SomeType (exists a. a) -- not valid GHC syntax
Die implizite oder explizite GADT-Syntax forallist für diese Typen einheitlicher und scheint leichter zu verstehen. Selbst mit einer expliziten forallDefinition vermittelt die folgende Definition die Idee, dass Sie einen Wert eines beliebigen Typs ain 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 Typeableoder Dataverwenden 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 Typeableoder 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 Workerzu fordern . Sie können eine Funktion schreiben, um eine mit einem bestimmten Typ von zu erstellen , wie z .WorkerBufferWorkerBufferMemoryBuffer
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 Workeras-Argument verwendet, kann sie nur die allgemeinen BufferTypklassenfunktionen (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 bum 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 Workerobige Typ hat zusätzlich zu den Feldern für den Puffer und die Eingabe auch ein unsichtbares implizites Feld, das auf das BufferWörterbuch verweist (ähnlich wie die V-Tabelle, obwohl sie kaum riesig ist, da sie nur einen Zeiger auf die entsprechende outputFunktion enthält).
Intern wird die Typklasse Bufferals 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 WorkerTyp optimiert. In diesem Beispiel enthält der existenzielle Typ ein verstecktes Feld, das aus einem Funktionszeiger auf die outputFunktion für den Puffer besteht. Dies ist die einzige erforderliche Laufzeitinformation von doWork.