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' ExistentialQuantification
extension, 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é forall
ici 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 exists
mot-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 a
et 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 Typeable
ou 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 Typeable
ou 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' Worker
avoir une Buffer
instance particulière . Vous pouvez écrire une fonction pour créer un en Worker
utilisant 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 Worker
argument as, elle ne peut utiliser que les Buffer
installations 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 b
s'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 Worker
type 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 Buffer
dictionnaire (un peu comme v-table, bien qu'il soit à peine énorme, car il contient juste un pointeur sur la output
fonction appropriée ).
En interne, la classe de type Buffer
est 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 doWork
celle 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 Worker
type existentiel inclut un champ caché qui se compose d'un pointeur de fonction vers la output
fonction du tampon, et c'est la seule information d'exécution nécessaire par doWork
.