Los GADT proporcionan la sintaxis clara y mejor para codificar usando Tipos Existenciales al proporcionar forall implícito
Creo que hay acuerdo general en que la sintaxis de GADT es mejor. No diría que se debe a que los GADT proporcionan foralls implícitos, sino más bien porque la sintaxis original, habilitada con la ExistentialQuantification
extensión, es potencialmente confusa / engañosa. Esa sintaxis, por supuesto, se ve así:
data SomeType = forall a. SomeType a
o con una restricción:
data SomeShowableType = forall a. Show a => SomeShowableType a
y creo que el consenso es que el uso de la palabra clave forall
aquí permite que el tipo se confunda fácilmente con el tipo completamente diferente:
data AnyType = AnyType (forall a. a) -- need RankNTypes extension
Una mejor sintaxis podría haber usado una exists
palabra clave separada , por lo que escribiría:
data SomeType = SomeType (exists a. a) -- not valid GHC syntax
La sintaxis GADT, ya sea que se use con implícito o explícito forall
, es más uniforme en todos estos tipos y parece ser más fácil de entender. Incluso con un explícito forall
, la siguiente definición transmite la idea de que puede tomar un valor de cualquier tipo a
y ponerlo dentro de un monomorfo SomeType'
:
data SomeType' where
SomeType' :: forall a. (a -> SomeType') -- parentheses optional
y es fácil ver y comprender la diferencia entre ese tipo y:
data AnyType' where
AnyType' :: (forall a. a) -> AnyType'
Los tipos existenciales no parecen estar interesados en el tipo que contienen, pero los patrones que coinciden con ellos dicen que existe algún tipo que no sabemos de qué tipo es y hasta que usemos Typeable o Data.
Los usamos cuando queremos Ocultar tipos (por ejemplo, para Listas heterogéneas) o realmente no sabemos cuáles son los tipos en tiempo de compilación.
Supongo que no están muy lejos, aunque no es necesario usar Typeable
o Data
usar tipos existenciales. Creo que sería más exacto decir que un tipo existencial proporciona un "cuadro" bien escrito alrededor de un tipo no especificado. El cuadro "oculta" el tipo en cierto sentido, lo que le permite hacer una lista heterogénea de dichos cuadros, ignorando los tipos que contienen. Resulta que un existencial sin restricciones, comoSomeType'
anterior, es bastante inútil, pero un tipo restringido:
data SomeShowableType' where
SomeShowableType' :: forall a. (Show a) => a -> SomeShowableType'
le permite hacer coincidir el patrón para mirar dentro de la "caja" y hacer que las instalaciones de clase de tipo estén disponibles:
showIt :: SomeShowableType' -> String
showIt (SomeShowableType' x) = show x
Tenga en cuenta que esto funciona para cualquier clase de tipo, no solo Typeable
o Data
.
Con respecto a su confusión acerca de la página 20 de la presentación de diapositivas, el autor dice que es imposible que una función que requiere una existencia Worker
exija una instancia Worker
particular Buffer
. Puede escribir una función para crear un Worker
uso de un tipo particular 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
pero si escribe una función que toma un Worker
argumento, solo puede usar las Buffer
instalaciones de clase de tipo general (por ejemplo, la funciónoutput
):
doWork :: Worker Int -> IO ()
doWork (Worker b x) = output (show x) b
No puede intentar exigir que b
sea un tipo particular de búfer, incluso a través de la coincidencia de patrones:
doWorkBroken :: Worker Int -> IO ()
doWorkBroken (Worker b x) = case b of
MemoryBuffer -> error "try this" -- type error
_ -> error "try that"
Finalmente, la información de tiempo de ejecución sobre los tipos existenciales está disponible a través de argumentos implícitos de "diccionario" para las clases de tipos involucradas. losWorker
tipo anterior, además de tener campos para el búfer y la entrada, también tiene un campo implícito invisible que apunta al Buffer
diccionario (algo así como v-table, aunque no es enorme, ya que solo contiene un puntero a la output
función apropiada ).
Internamente, la clase de tipo Buffer
se representa como un tipo de datos con campos de función, y las instancias son "diccionarios" de este tipo:
data Buffer' b = Buffer' { output' :: String -> b -> IO () }
dBuffer_MemoryBuffer :: Buffer' MemoryBuffer
dBuffer_MemoryBuffer = Buffer' { output' = undefined }
El tipo existencial tiene un campo oculto para este diccionario:
data Worker' x = forall b. Worker' { dBuffer :: Buffer' b, buffer' :: b, input' :: x }
y una función como doWork
esa opera en Worker'
valores existenciales se implementa como:
doWork' :: Worker' Int -> IO ()
doWork' (Worker' dBuf b x) = output' dBuf (show x) b
Para una clase de tipo con una sola función, el diccionario está optimizado para un nuevo tipo, por lo que en este ejemplo, el Worker
tipo existencial incluye un campo oculto que consiste en un puntero de función a la output
función para el búfer, y esa es la única información de tiempo de ejecución necesaria por doWork
.