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
.
AnyType
tipo 2; isso é confuso e eu o apaguei. O construtorAnyType
atua como uma função de classificação 2 e o construtorSomeType
atua como uma função de classificação 1 (assim como a maioria dos tipos não existentes ), mas essa não é uma caracterização muito útil. De qualquer forma, o que torna esses tipos interessantes é que eles são de classificação 0 (ou seja, não quantificados sobre uma variável de tipo e são monomórficos), mesmo que "contenham" tipos quantificados.Como
Worker
, conforme definido, leva apenas um argumento, o tipo do campo "entrada" (variável de tipox
). Por exemplo,Worker Int
é um tipo. A variável typeb
, em vez disso, não é um parâmetro deWorker
, mas é uma espécie de "variável local", por assim dizer. Não pode ser passado como emWorker Int String
- isso provocaria um erro de tipo.Se, em vez disso, definimos:
então
Worker Int String
funcionaria, mas o tipo não é mais existencial - agora sempre precisamos passar também o tipo de buffer.Isso está aproximadamente correto. Resumidamente, toda vez que você aplica o construtor
Worker
, o GHC deduz ob
tipo dos argumentos deWorker
e depois procura uma instânciaBuffer b
. Se isso for encontrado, o GHC inclui um ponteiro adicional para a instância no objeto. Na sua forma mais simples, isso não é muito diferente do "ponteiro para a tabela" que é adicionado a cada objeto no OOP quando funções virtuais estão presentes.No caso geral, pode ser muito mais complexo. O compilador pode usar uma representação diferente e adicionar mais ponteiros em vez de um único (por exemplo, adicionar diretamente os ponteiros a todos os métodos de instância), se isso acelerar o código. Além disso, às vezes o compilador precisa usar várias instâncias para satisfazer uma restrição. Por exemplo, se precisamos armazenar a instância para
Eq [Int]
... então não há uma, mas duas: uma paraInt
e uma para listas, e as duas precisam ser combinadas (em tempo de execução, exceto otimizações).É difícil adivinhar exatamente o que o GHC faz em cada caso: isso depende de uma tonelada de otimizações que podem ou não ser desencadeadas.
Você pode tentar pesquisar na implementação "baseada em dicionário" das classes de tipos para ver mais sobre o que está acontecendo. Você também pode pedir ao GHC para imprimir o Core otimizado interno
-ddump-simpl
e observar os dicionários que estão sendo construídos, armazenados e distribuídos. Preciso avisá-lo: o núcleo é de nível bastante baixo e pode ser difícil de ler a princípio.fonte