Pergunta

Como faço uso do tipo de dados algébricos generalizados?

O exemplo dado no Wikilivro de Haskell é demasiado curto para me dar uma ideia das reais possibilidades do GADT.

Foi útil?

Solução

Eu encontrei a mônada "Prompt" (do pacote "Monadprompt") uma ferramenta muito útil em vários lugares (juntamente com o equivalente "Monad" do pacote "operacional". Combinado com Gadts (e é assim que se destinava a ser usado), ele permite que você faça idiomas incorporados de maneira muito barata e de maneira muito flexível. Havia um artigo muito bom no Monad Reader Edição 15 chamado "aventuras em três mônadas", que tiveram uma boa introdução à mônada rápida, juntamente com alguns gadões realistas.

Outras dicas

Os Gadts são aproximações fracas de famílias indutivas de idiomas digitados dependentemente - então vamos começar por lá.

As famílias indutivas são o método principal de introdução de dados de dados em um idioma digitado dependentemente. Por exemplo, na AGDA, você define os números naturais como este

data Nat : Set where
  zero : Nat
  succ : Nat -> Nat 

O que não é muito chique, é essencialmente a mesma coisa que a definição de Haskell

data Nat = Zero | Succ Nat

e de fato na sintaxe de gadt, a forma haskell é ainda mais semelhante

{-# LANGUAGE GADTs #-}

data Nat where
  Zero :: Nat
  Succ :: Nat -> Nat

Então, à primeira vista, você pode pensar que os Gadts são apenas uma sintaxe extra. Essa é apenas a ponta do iceberg.


A AGDA tem capacidade para representar todos os tipos de tipos não familiares e estranhos para um programador Haskell. Um simples é o tipo de conjunto finito. este modelo está escrito como Fin 3 e representa o definir de números {0, 1, 2}. Da mesma maneira, Fin 5 representa o conjunto de números {0,1,2,3,4}.

Isso deve ser bastante bizarro neste momento. Primeiro, estamos nos referindo a um tipo que possui um número regular como um parâmetro "tipo". Segundo, não está claro o que significa para Fin n Para representar o conjunto {0,1...n}. No verdadeiro AGDA, faríamos algo mais poderoso, mas basta dizer que podemos definir um contains função

contains : Nat -> Fin n -> Bool
contains i f = ?

Agora isso é estranho novamente porque a definição "natural" de contains seria algo como i < n, mas n é um valor que só existe no tipo Fin n E não devemos ser capazes de atravessar essa divisão tão facilmente. Embora a definição não seja tão direta, esse é exatamente o poder que as famílias indutivas digitam dependentemente idiomas - elas introduzem valores que dependem de seus tipos e tipos que dependem de seus valores.


Podemos examinar o que se trata Fin Isso concede essa propriedade olhando para sua definição.

data Fin : Nat -> Set where
  zerof : (n : Nat) -> Fin (succ n)
  succf : (n : Nat) -> (i : Fin n) -> Fin (succ n)

Isso exige um pouco de trabalho para entender, então, como exemplo, vamos tentar construir um valor do tipo Fin 2. Existem algumas maneiras de fazer isso (na verdade, descobriremos que existem exatamente 2)

zerof 1           : Fin 2
zerof 2           : Fin 3 -- nope!
zerof 0           : Fin 1 -- nope!
succf 1 (zerof 0) : Fin 2

Isso nos permite ver que existem dois habitantes e também demonstra um pouco de como o computação do tipo acontece. Em particular, o (n : Nat) bit no tipo de zerof reflete o real valor n até o tipo que nos permite formar Fin (n+1) para qualquer n : Nat. Depois disso, usamos aplicações repetidas de succf para incrementar nosso Fin Valores até o índice familiar do tipo correto (número natural que indexa o Fin).

O que fornece essas habilidades? Com toda a honestidade, existem muitas diferenças entre uma família indutiva digitada dependentemente e um Haskell ADT regular, mas podemos nos concentrar no exato que é mais relevante para entender os Gadts.

Em Gadts e famílias indutivas, você tem a oportunidade de especificar o exato tipo de seus construtores. Isso pode ser chato

data Nat where
  Zero :: Nat
  Succ :: Nat -> Nat

Ou, se tivermos um tipo mais flexível e indexado, podemos escolher diferentes tipos de retorno mais interessantes

data Typed t where
  TyInt  :: Int                -> Typed Int
  TyChar :: Char               -> Typed Char
  TyUnit ::                       Typed ()
  TyProd :: Typed a -> Typed b -> Typed (a, b)
  ...

Em particular, estamos abusando da capacidade de modificar o tipo de retorno com base no especial construtor de valor usado. Isso nos permite refletir algumas informações de valor no tipo e produzir mais especificados (fibra) digitados.


Então, o que podemos fazer com eles? Bem, com um pouco de graxa de cotovelo, podemos produzir Fin em Haskell. Sucintamente, é necessário que definimos uma noção de naturais nos tipos

data Z
data S a = S a

> undefined :: S (S (S Z))  -- 3

... então um gadt para refletir valores desses tipos ...

data Nat where
  Zero :: Nat Z
  Succ :: Nat n -> Nat (S n)

... então podemos usá -los para construir Fin Muito parecido com o AGDA ...

data Fin n where
  ZeroF :: Nat n -> Fin (S n)
  SuccF :: Nat n -> Fin n -> Fin (S n)

E finalmente podemos construir exatamente dois valores de Fin (S (S Z))

*Fin> :t ZeroF (Succ Zero)
ZeroF (Succ Zero) :: Fin (S (S Z))

*Fin> :t SuccF (Succ Zero) (ZeroF Zero)
SuccF (Succ Zero) (ZeroF Zero) :: Fin (S (S Z))

Mas observe que perdemos muita conveniência sobre as famílias indutivas. Por exemplo, não podemos usar literais numéricos regulares em nossos tipos (embora isso seja tecnicamente apenas um truque na AGDA de qualquer maneira), precisamos criar um "tipo NAT" e "Value Nat" separado e usar o gadt para vinculá -los, E também descobriríamos, com o tempo, que, embora o tipo de matemática seja doloroso na AGDA, isso pode ser feito. Em Haskell, é incrivelmente doloroso e muitas vezes não pode.

Por exemplo, é possível definir um weaken noção em AGDA Fin modelo

weaken : (n <= m) -> Fin n -> Fin m
weaken = ...

onde fornecemos um primeiro valor muito interessante, uma prova de que n <= m o que nos permite incorporar "um valor menor que n"no conjunto de" valores menos que m". Podemos fazer o mesmo em Haskell, tecnicamente, mas requer abuso pesado de próLo de classe de tipo.


Portanto, os Gadts são uma semelhança de famílias indutivas em idiomas digitados dependentemente que são cada vez mais fracos. Por que os queremos em Haskell em primeiro lugar?

Basicamente, porque nem todos os invariantes de tipo exigem que todo o poder das famílias indutivas expresse e os GADTs escolham um compromisso particular entre expressividade, implementabilidade em Haskell e inferência de tipo.

Alguns exemplos de expressões de Gadts úteis são Árvores vermelhas que não podem ter a propriedade vermelha-preta invalidada ou cálculo de lambda simplesmente tipado incorporado como bastão de porquinho do sistema do tipo Haskell.

Na prática, você também costuma ver o uso de GADTs para o seu contexto existencial implícito. Por exemplo, o tipo

data Foo where
  Bar :: a -> Foo

esconde implicitamente o a Digite variável usando quantificação existencial

> :t Bar 4 :: Foo

de uma maneira que às vezes é conveniente. Se você olhar com cuidado, o exemplo hoas da Wikipedia usa isso para o a Tipo parâmetro no App construtor. Expressar essa afirmação sem Gadts seria uma bagunça de contextos existenciais, mas a sintaxe do gadt a torna natural.

Os Gadts podem oferecer garantias forçadas do tipo mais forte do que o ADTS regular. Por exemplo, você pode forçar uma árvore binária a ser equilibrada no nível do sistema de tipos, como em esta implementação do 2-3 árvores:

{-# LANGUAGE GADTs #-}

data Zero
data Succ s = Succ s

data Node s a where
    Leaf2 :: a -> Node Zero a
    Leaf3 :: a -> a -> Node Zero a
    Node2 :: Node s a -> a -> Node s a -> Node (Succ s) a
    Node3 :: Node s a -> a -> Node s a -> a -> Node s a -> Node (Succ s) a

Cada nó tem uma profundidade codificada por tipo onde todas as suas folhas residem. Uma árvore é então uma árvore vazia, um valor de singleton ou um nó de profundidade não especificada, novamente usando os Gadts.

data BTree a where
    Root0 :: BTree a
    Root1 :: a -> BTree a
    RootN :: Node s a -> BTree a

O sistema de tipos garante que apenas nós equilibrados possam ser construídos. Isso significa que ao implementar operações como insert Nessas árvores, o seu código verifica apenas se seu resultado for sempre uma árvore equilibrada.

Eu gosto do exemplo em o manual do GHC. É uma demonstração rápida de uma idéia do Gadt Core: que você pode incorporar o sistema de tipo de um idioma que está manipulando no sistema de tipos de Haskell. Isso permite que suas funções Haskell assumam e as obriga a preservar, que as árvores de sintaxe correspondam a programas bem titados.

Quando definimos Term, não importa quais tipos escolhemos. Nós poderíamos escrever

data Term a where
  ...
  IsZero :: Term Char -> Term Char

ou

  ...
  IsZero :: Term a -> Term b

e a definição de Term ainda passaria.

É só quando quisermos Calcule em Term, como em definir eval, que os tipos importam. Precisamos ter

  ...
  IsZero :: Term Int -> Term Bool

Porque precisamos de nossa chamada recursiva para eval para devolver um Int, e queremos, por sua vez, retornar um Bool.

Esta é uma resposta curta, mas consulte o Haskell Wikibook.Ele orienta você em um GADT para uma árvore de expressão bem digitada, que é um exemplo bastante canônico: http://en.wikibooks.org/wiki/Haskell/GADT

GADTs também são usados ​​para implementar igualdade de tipos: http://hackage.haskell.org/package/type-equality.Não consigo encontrar o artigo certo para fazer referência a isso de improviso - essa técnica já se tornou popular no folclore.É usado muito bem, entretanto, nas coisas sem tags digitadas por Oleg.Veja, por exemploa seção sobre compilação digitada em GADTs. http://okmij.org/ftp/tagless-final/#tc-GADT

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top