GADT fornisce la sintassi chiara e migliore per codificare usando i tipi esistenziali fornendo forall impliciti
Penso che ci sia un accordo generale sul fatto che la sintassi di GADT sia migliore. Non direi che è perché i GADT forniscono forks impliciti, ma piuttosto perché la sintassi originale, abilitata con l' ExistentialQuantification
estensione, è potenzialmente confusa / fuorviante. Tale sintassi, ovviamente, assomiglia a:
data SomeType = forall a. SomeType a
o con un vincolo:
data SomeShowableType = forall a. Show a => SomeShowableType a
e penso che il consenso sia che l'uso della parola chiave forall
qui permetta al tipo di essere facilmente confuso con il tipo completamente diverso:
data AnyType = AnyType (forall a. a) -- need RankNTypes extension
Una sintassi migliore avrebbe potuto usare una exists
parola chiave separata , quindi dovresti scrivere:
data SomeType = SomeType (exists a. a) -- not valid GHC syntax
La sintassi GADT, usata con implicito o esplicito forall
, è più uniforme tra questi tipi e sembra essere più facile da capire. Anche con un esplicito forall
, la seguente definizione porta l'idea che puoi prendere un valore di qualsiasi tipo a
e inserirlo in un monomorfo SomeType'
:
data SomeType' where
SomeType' :: forall a. (a -> SomeType') -- parentheses optional
ed è facile vedere e comprendere la differenza tra quel tipo e:
data AnyType' where
AnyType' :: (forall a. a) -> AnyType'
I tipi esistenziali non sembrano essere interessati al tipo che contengono, ma i pattern corrispondenti ad essi dicono che esiste un tipo che non conosciamo fino a quando non usiamo Typeable o Data.
Li usiamo quando vogliamo nascondere i tipi (es: per gli elenchi eterogenei) o non sappiamo davvero quali siano i tipi al momento della compilazione.
Immagino che questi non siano troppo lontani, anche se non devi usare Typeable
o Data
usare tipi esistenziali. Penso che sarebbe più preciso dire che un tipo esistenziale fornisce una "scatola" ben tipizzata attorno a un tipo non specificato. La casella "nasconde" il tipo in un certo senso, che consente di creare un elenco eterogeneo di tali caselle, ignorando i tipi che contengono. Si scopre che un esistenziale non vincolato, come SomeType'
sopra, è piuttosto inutile, ma un tipo vincolato:
data SomeShowableType' where
SomeShowableType' :: forall a. (Show a) => a -> SomeShowableType'
ti permette di modellare la corrispondenza per sbirciare all'interno della "scatola" e rendere disponibili le strutture di tipo classe:
showIt :: SomeShowableType' -> String
showIt (SomeShowableType' x) = show x
Nota che questo funziona per qualsiasi classe di tipo, non solo Typeable
o Data
.
Per quanto riguarda la tua confusione sulla pagina 20 del deck diapositive, l'autore sta dicendo che è impossibile per una funzione che richiede un esistenziale Worker
richiedere un'istanza Worker
particolare Buffer
. Puoi scrivere una funzione per crearne una Worker
usando un tipo particolare di Buffer
, come 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
ma se scrivi una funzione che accetta un Worker
argomento come, può usare solo le Buffer
strutture di classe di tipo generale (ad es. la funzione output
):
doWork :: Worker Int -> IO ()
doWork (Worker b x) = output (show x) b
Non può provare a richiedere che si b
tratti di un particolare tipo di buffer, anche tramite la corrispondenza dei modelli:
doWorkBroken :: Worker Int -> IO ()
doWorkBroken (Worker b x) = case b of
MemoryBuffer -> error "try this" -- type error
_ -> error "try that"
Infine, le informazioni di runtime sui tipi esistenziali sono rese disponibili attraverso argomenti impliciti "dizionario" per le typeclass coinvolte. Il Worker
tipo sopra, oltre ad avere campi per il buffer e l'input, ha anche un campo implicito invisibile che punta al Buffer
dizionario (un po 'come v-table, anche se non è enorme, poiché contiene solo un puntatore alla output
funzione appropriata ).
Internamente, la classe type Buffer
è rappresentata come un tipo di dati con campi funzione e le istanze sono "dizionari" di questo tipo:
data Buffer' b = Buffer' { output' :: String -> b -> IO () }
dBuffer_MemoryBuffer :: Buffer' MemoryBuffer
dBuffer_MemoryBuffer = Buffer' { output' = undefined }
Il tipo esistenziale ha un campo nascosto per questo dizionario:
data Worker' x = forall b. Worker' { dBuffer :: Buffer' b, buffer' :: b, input' :: x }
e una funzione come doWork
quella opera su Worker'
valori esistenziali è implementata come:
doWork' :: Worker' Int -> IO ()
doWork' (Worker' dBuf b x) = output' dBuf (show x) b
Per una classe di tipo con una sola funzione, il dizionario è effettivamente ottimizzato su un nuovo tipo, quindi in questo esempio il Worker
tipo esistenziale include un campo nascosto che consiste in un puntatore alla output
funzione per il buffer, e questa è l'unica informazione di runtime necessaria di doWork
.