Os GADTs fornecem a sintaxe clara e melhor para codificar usando Tipos Existenciais, fornecendo
Eu acho que há um consenso geral de que a sintaxe do GADT é melhor. Eu não diria que é porque os GADTs fornecem formas implícitas, mas sim porque a sintaxe original, ativada com a ExistentialQuantification
extensão, é potencialmente confusa / enganosa. Essa sintaxe, é claro, se parece com:
data SomeType = forall a. SomeType a
ou com uma restrição:
data SomeShowableType = forall a. Show a => SomeShowableType a
e acho que o consenso é que o uso da palavra-chave forall
aqui permite que o tipo seja facilmente confundido com o tipo completamente diferente:
data AnyType = AnyType (forall a. a) -- need RankNTypes extension
Uma sintaxe melhor pode ter usado uma exists
palavra-chave separada , então você deve escrever:
data SomeType = SomeType (exists a. a) -- not valid GHC syntax
A sintaxe do GADT, usada com implícita ou explícita forall
, é mais uniforme entre esses tipos e parece ser mais fácil de entender. Mesmo com um explícito forall
, a seguinte definição transmite a ideia de que você pode pegar um valor de qualquer tipo a
e colocá-lo dentro de um monomórfico SomeType'
:
data SomeType' where
SomeType' :: forall a. (a -> SomeType') -- parentheses optional
e é fácil ver e entender a diferença entre esse tipo e:
data AnyType' where
AnyType' :: (forall a. a) -> AnyType'
Tipos existentes não parecem estar interessados no tipo que eles contêm, mas os padrões correspondentes dizem que existe algum tipo que não sabemos qual é o tipo até & a menos que usemos Typeable ou Data.
Nós os usamos quando queremos ocultar tipos (por exemplo, para listas heterogêneas) ou realmente não sabemos quais são os tipos em tempo de compilação.
Eu acho que eles não estão muito longe, embora você não precise usar Typeable
ou Data
usar tipos existenciais. Eu acho que seria mais preciso dizer que um tipo existencial fornece uma "caixa" bem digitada em torno de um tipo não especificado. A caixa "oculta" o tipo em um sentido, o que permite fazer uma lista heterogênea dessas caixas, ignorando os tipos que elas contêm. Acontece que um existencial irrestrito, como SomeType'
acima, é bastante inútil, mas um tipo restrito:
data SomeShowableType' where
SomeShowableType' :: forall a. (Show a) => a -> SomeShowableType'
permite padronizar a correspondência para espiar dentro da "caixa" e disponibilizar as facilidades da classe de tipo:
showIt :: SomeShowableType' -> String
showIt (SomeShowableType' x) = show x
Observe que isso funciona para qualquer classe de tipo, não apenas Typeable
ou Data
.
Com relação à sua confusão sobre a página 20 do deck de slides, o autor está dizendo que é impossível para uma função que exista um existencial Worker
exigir uma instância Worker
específica Buffer
. Você pode escrever uma função para criar uma Worker
usando um tipo específico de Buffer
, como 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
mas se você escrever uma função que usa um Worker
argumento as, ela poderá usar apenas os Buffer
recursos da classe de tipo geral (por exemplo, a função output
):
doWork :: Worker Int -> IO ()
doWork (Worker b x) = output (show x) b
Ele não pode tentar exigir que b
seja um tipo específico de buffer, mesmo através da correspondência de padrões:
doWorkBroken :: Worker Int -> IO ()
doWorkBroken (Worker b x) = case b of
MemoryBuffer -> error "try this" -- type error
_ -> error "try that"
Por fim, informações de tempo de execução sobre tipos existenciais são disponibilizadas por meio de argumentos implícitos de "dicionário" para as classes de tipos envolvidas. O Worker
tipo acima, além de ter campos para o buffer e a entrada, também possui um campo implícito invisível que aponta para o Buffer
dicionário (um pouco como a tabela v, embora seja dificilmente enorme, pois contém apenas um ponteiro para a output
função apropriada ).
Internamente, a classe de tipo Buffer
é representada como um tipo de dados com campos de função e as instâncias são "dicionários" desse tipo:
data Buffer' b = Buffer' { output' :: String -> b -> IO () }
dBuffer_MemoryBuffer :: Buffer' MemoryBuffer
dBuffer_MemoryBuffer = Buffer' { output' = undefined }
O tipo existencial possui um campo oculto para este dicionário:
data Worker' x = forall b. Worker' { dBuffer :: Buffer' b, buffer' :: b, input' :: x }
e uma função como doWork
essa opera em Worker'
valores existenciais é implementada como:
doWork' :: Worker' Int -> IO ()
doWork' (Worker' dBuf b x) = output' dBuf (show x) b
Para uma classe de tipo com apenas uma função, o dicionário é realmente otimizado para um novo tipo; portanto, neste exemplo, o Worker
tipo existencial inclui um campo oculto que consiste em um ponteiro de função para a output
função do buffer e essa é a única informação de tempo de execução necessária por doWork
.