haskell gadt

haskell - Uso del mundo real de GADT



(5)

Esta es una respuesta corta, pero consulte el Haskell Wikilibro. Te guía a través de un GADT para un árbol de expresiones bien tipado, que es un ejemplo bastante canónico: http://en.wikibooks.org/wiki/Haskell/GADT

Las GADT también se usan para implementar la igualdad de tipo: http://hackage.haskell.org/package/type-equality . No puedo encontrar el papel adecuado para referirme a esto de improviso: esta técnica ha llegado al folclore en este momento. Sin embargo, se usa bastante bien en las cosas sin etiquetas de Oleg. Ver, por ejemplo, la sección de compilación tipada en GADTs. http://okmij.org/ftp/tagless-final/#tc-GADT

¿Hay algún recurso bueno sobre el uso real del tipo de datos algebraicos generalizados?

El ejemplo dado en el haskell wikibook es demasiado corto para darme una idea de las posibilidades reales de GADT.

Gracias


He encontrado que la mónada "Prompt" (del paquete "MonadPrompt") es una herramienta muy útil en varios lugares (junto con la mónada de "Programa" equivalente del paquete "operacional". Combinado con GADT (que es cómo se pretendía) ser usado), le permite crear lenguajes incrustados a muy bajo costo y muy flexible. Hubo un artículo bastante bueno en el número 15 de Monad Reader llamado "Aventuras en tres mónadas" que tuvo una buena introducción a la mónada de avisos junto con algunos GADT realistas .


Las GADT pueden otorgarle garantías reforzadas de tipo más fuerte que las ADT normales. Por ejemplo, puede forzar que un árbol binario se equilibre en el nivel de sistema de tipo, como en esta implementación de 2-3 árboles :

{-# 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 nodo tiene una profundidad de tipo codificada donde residen todas sus hojas. Un árbol es entonces un árbol vacío, un valor singleton o un nodo de profundidad no especificada, de nuevo usando GADT.

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

El sistema de tipo le garantiza que solo se pueden construir nodos equilibrados. Esto significa que al implementar operaciones como insert en dichos árboles, su tipo de código comprueba solo si su resultado es siempre un árbol equilibrado.


Las GADT son aproximaciones débiles de las familias inductivas a partir de los idiomas tipados de manera dependiente, así que comencemos en su lugar.

Las familias inductivas son el método central de introducción de datos en un lenguaje dependiente. Por ejemplo, en Agda defines los números naturales como este

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

que no es muy elegante, es esencialmente lo mismo que la definición de Haskell

data Nat = Zero | Succ Nat

y de hecho en la sintaxis GADT, la forma Haskell es aún más similar

{-# LANGUAGE GADTs #-} data Nat where Zero :: Nat Succ :: Nat -> Nat

Por lo tanto, a primera vista, puede pensar que las GADT son simplemente una sintaxis extra ordenada. Sin embargo, eso es solo la punta del iceberg.

Agda tiene la capacidad de representar todo tipo de tipos desconocidos y extraños para un programador Haskell. Una simple es el tipo de conjuntos finitos. Este tipo está escrito como Fin 3 y representa el conjunto de números {0, 1, 2} . Del mismo modo, Fin 5 representa el conjunto de números {0,1,2,3,4} .

Esto debería ser bastante extraño en este punto. Primero, nos referimos a un tipo que tiene un número regular como parámetro de "tipo". En segundo lugar, no está claro qué significa para Fin n representar el conjunto {0,1...n} . En Agda real haríamos algo más poderoso, pero basta decir que podemos definir una función contains

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

Ahora, esto es extraño de nuevo porque la definición "natural" de contains sería algo como i < n , pero n es un valor que solo existe en el tipo Fin n y no deberíamos poder cruzar esa división tan fácilmente. Si bien resulta que la definición no es tan directa, este es exactamente el poder que tienen las familias inductivas en los idiomas tipados de manera dependiente: introducen valores que dependen de sus tipos y tipos que dependen de sus valores.

Podemos examinar de qué se trata Fin que le da esa propiedad al observar su definición.

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

esto requiere un poco de trabajo para comprender, así que como ejemplo, intentemos construir un valor del tipo Fin 2 . Hay algunas formas de hacerlo (de hecho, descubriremos que hay exactamente 2)

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

Esto nos permite ver que hay dos habitantes y también demuestra un poco de cómo ocurre el cálculo de tipo. En particular, el bit (n : Nat) en el tipo de zerof refleja el valor real n arriba en el tipo que nos permite formar Fin (n+1) para cualquier n : Nat . Después de eso, utilizamos repetidas aplicaciones de succf para incrementar nuestros valores de Fin hasta el índice de familia de tipo correcto (número natural que indexa el Fin ).

¿Qué proporciona estas habilidades? Honestamente, hay muchas diferencias entre una familia inductiva de tipo dependiente y una ADT de Haskell regular, pero podemos enfocarnos en la que sea más relevante para comprender las GADT.

En GADTs y familias inductivas tienes la oportunidad de especificar el tipo exacto de tus constructores. Esto podría ser aburrido

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

O bien, si tenemos un tipo indexado más flexible, podemos elegir diferentes tipos de retorno más interesantes

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

En particular, estamos abusando de la capacidad de modificar el tipo de devolución en función del constructor de valor particular utilizado. Esto nos permite reflejar cierta información de valor en el tipo y producir tipeado (fibered) más finamente especificado.

Entonces, ¿qué podemos hacer con ellos? Bueno, con un poco de grasa en el codo podemos producir Fin en Haskell . De manera sucinta requiere que definamos una noción de naturales en tipos

data Z data S a = S a > undefined :: S (S (S Z)) -- 3

... luego un GADT para reflejar los valores en esos tipos ...

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

... entonces podemos usar estos para construir Fin como lo hicimos en Agda ...

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

Y finalmente podemos construir exactamente dos valores de Fin (S (SZ))

*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))

Pero note que hemos perdido mucha conveniencia sobre las familias inductivas. Por ejemplo, no podemos usar literales numéricos regulares en nuestros tipos (aunque técnicamente es solo un truco en Agda), tenemos que crear un "nat tipo" y "valor nat" por separado y usar el GADT para unirlos, y también encontraríamos, a tiempo, que mientras que las matemáticas de nivel de tipo son dolorosas en Agda, se puede hacer. En Haskell es increíblemente doloroso y a menudo no puede.

Por ejemplo, es posible definir una noción de weaken en el tipo de Fin de Agda

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

donde proporcionamos un primer valor muy interesante, una prueba de que n <= m que nos permite insertar "un valor menor que n " en el conjunto de "valores menores que m ". Podemos hacer lo mismo en Haskell, técnicamente, pero requiere un gran abuso del prólogo de clase de tipo.

Entonces, las GADT son una semejanza de las familias inductivas en los idiomas dependientes que son más débiles y torpes. ¿Por qué los queremos en Haskell en primer lugar?

Básicamente porque no todas las invariantes de tipo requieren toda la potencia de las familias inductivas para expresarse y las GADT eligen un compromiso particular entre la expresividad, la implementabilidad en Haskell y la inferencia tipográfica.

Algunos ejemplos de expresiones GADT útiles son Árboles Rojo-Negro que no pueden tener la propiedad Rojo-Negro invalidada o simplemente cálculo lambda integrado como HOAS piggy-backing del sistema de tipo Haskell .

En la práctica, también suele ver el uso de GADT en su contexto existencial implícito. Por ejemplo, el tipo

data Foo where Bar :: a -> Foo

oculta implícitamente la variable tipo a mediante cuantificación existencial

> :t Bar 4 :: Foo

de una manera que a veces es conveniente. Si observa detenidamente el ejemplo HOAS de Wikipedia usa esto para el parámetro a type en el constructor de la App . Expresar esa afirmación sin GADT sería un desastre de contextos existenciales, pero la sintaxis de GADT lo hace natural.


Me gusta el ejemplo en el manual de GHC . Es una demostración rápida de una idea central de GADT: que puede insertar el sistema de tipos de un idioma que está manipulando en el sistema de tipos de Haskell. Esto permite que sus funciones de Haskell asuman, y las obligue a preservar, que los árboles de sintaxis corresponden a programas bien tipados.

Cuando definimos Term , no importa qué tipos elijamos. Podríamos escribir

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

o

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

y la definición de Term todavía pasaría.

Es solo cuando queremos calcular en Term , como en la definición de eval , que los tipos importan. Necesitamos tener

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

porque necesitamos nuestra llamada recursiva a eval para devolver un Int , y queremos a cambio devolver un Bool .