Les GADT fournissent la syntaxe claire et meilleure pour coder en utilisant les types existentiels en fournissant des for implicites
Je pense qu'il est généralement admis que la syntaxe GADT est meilleure. Je ne dirais pas que c'est parce que les GADT fournissent des foralls implicites, mais plutôt parce que la syntaxe originale, activée avec l' ExistentialQuantificationextension, est potentiellement déroutante / trompeuse. Cette syntaxe, bien sûr, ressemble à:
data SomeType = forall a. SomeType a
ou avec une contrainte:
data SomeShowableType = forall a. Show a => SomeShowableType a
et je pense que le consensus est que l'utilisation du mot-clé forallici permet de confondre facilement le type avec le type complètement différent:
data AnyType = AnyType (forall a. a) -- need RankNTypes extension
Une meilleure syntaxe aurait pu utiliser un existsmot-clé distinct , vous écririez donc:
data SomeType = SomeType (exists a. a) -- not valid GHC syntax
La syntaxe GADT, qu'elle soit utilisée avec implicite ou explicite forall, est plus uniforme entre ces types et semble plus facile à comprendre. Même avec un explicite forall, la définition suivante passe à travers l'idée que vous pouvez prendre une valeur de n'importe quel type aet la mettre dans un monomorphe SomeType':
data SomeType' where
SomeType' :: forall a. (a -> SomeType') -- parentheses optional
et il est facile de voir et de comprendre la différence entre ce type et:
data AnyType' where
AnyType' :: (forall a. a) -> AnyType'
Les types existentiels ne semblent pas être intéressés par le type qu'ils contiennent, mais les modèles qui les correspondent disent qu'il existe un type, nous ne savons pas de quel type il s'agit tant que nous n'utilisons pas Typeable ou Data.
Nous les utilisons lorsque nous voulons masquer des types (ex: pour les listes hétérogènes) ou nous ne savons pas vraiment quels types au moment de la compilation.
Je suppose que ce n'est pas trop loin, bien que vous n'ayez pas à utiliser Typeableou Dataà utiliser des types existentiels. Je pense qu'il serait plus précis de dire qu'un type existentiel fournit une "boîte" bien typée autour d'un type non spécifié. La boîte "cache" le type dans un sens, ce qui vous permet de faire une liste hétérogène de ces boîtes, en ignorant les types qu'elles contiennent. Il s'avère qu'un existentiel non contraint, comme SomeType'ci-dessus, est assez inutile, mais un type contraint:
data SomeShowableType' where
SomeShowableType' :: forall a. (Show a) => a -> SomeShowableType'
vous permet de faire correspondre les motifs pour jeter un œil à l'intérieur de la "boîte" et de rendre les installations de classe de type disponibles:
showIt :: SomeShowableType' -> String
showIt (SomeShowableType' x) = show x
Notez que cela fonctionne pour n'importe quelle classe de type, pas seulement Typeableou Data.
En ce qui concerne votre confusion à propos de la page 20 du diaporama, l'auteur dit qu'il est impossible pour une fonction qui prend un existentiel Worker d'exiger d' Workeravoir une Bufferinstance particulière . Vous pouvez écrire une fonction pour créer un en Workerutilisant un type particulier de Buffer, comme 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
mais si vous écrivez une fonction qui prend un Workerargument as, elle ne peut utiliser que les Bufferinstallations de classe de type général (par exemple, la fonction output):
doWork :: Worker Int -> IO ()
doWork (Worker b x) = output (show x) b
Il ne peut pas essayer d'exiger qu'il bs'agisse d'un type particulier de tampon, même via la correspondance de modèles:
doWorkBroken :: Worker Int -> IO ()
doWorkBroken (Worker b x) = case b of
MemoryBuffer -> error "try this" -- type error
_ -> error "try that"
Enfin, les informations d'exécution sur les types existentiels sont mises à disposition via des arguments implicites de "dictionnaire" pour les classes de types impliquées. Le Workertype ci-dessus, en plus d'avoir des champs pour le tampon et l'entrée, a également un champ implicite invisible qui pointe vers le Bufferdictionnaire (un peu comme v-table, bien qu'il soit à peine énorme, car il contient juste un pointeur sur la outputfonction appropriée ).
En interne, la classe de type Bufferest représentée comme un type de données avec des champs de fonction et les instances sont des "dictionnaires" de ce type:
data Buffer' b = Buffer' { output' :: String -> b -> IO () }
dBuffer_MemoryBuffer :: Buffer' MemoryBuffer
dBuffer_MemoryBuffer = Buffer' { output' = undefined }
Le type existentiel a un champ caché pour ce dictionnaire:
data Worker' x = forall b. Worker' { dBuffer :: Buffer' b, buffer' :: b, input' :: x }
et une fonction comme doWorkcelle qui opère sur des Worker'valeurs existentielles est implémentée comme:
doWork' :: Worker' Int -> IO ()
doWork' (Worker' dBuf b x) = output' dBuf (show x) b
Pour une classe de type avec une seule fonction, le dictionnaire est en fait optimisé pour un nouveau type, donc dans cet exemple, le Workertype existentiel inclut un champ caché qui se compose d'un pointeur de fonction vers la outputfonction du tampon, et c'est la seule information d'exécution nécessaire par doWork.