sirven semánticas semantica qué que para nuevos las etiquetas elementos ejemplos contenedores haskell types containers

haskell - semánticas - Tipos asociados y elementos contenedores.



semantica html5 (2)

Creo que quizás haya preguntado esto en Haskell-Cafe en algún momento, pero maldita sea si puedo encontrar la respuesta ahora ... Así que lo estoy pidiendo nuevamente aquí, así que espero que en el futuro pueda encontrar la respuesta

Haskell es fantástico en el tratamiento del polimorfismo paramétrico. Pero el problema es que no todo es paramétrico. Como ejemplo trivial, supongamos que queremos obtener el primer elemento de datos de un contenedor. Para un tipo paramétrico, eso es trivial:

class HasFirst c where first :: c x -> Maybe x instance HasFirst [] where first [] = Nothing first (x:_) = Just x

Ahora intenta y escribe una instancia para ByteString . Usted no puede Su tipo no menciona el tipo de elemento. Tampoco puede escribir una instancia para Set , porque requiere una restricción Ord , pero el jefe de clase no menciona el tipo de elemento, por lo que no puede restringirlo.

Los tipos asociados proporcionan una forma ordenada de solucionar completamente estos problemas:

class HasFirst c where type Element c :: * first :: c -> Maybe (Element c) instance HasFirst [x] where type Element [x] = x first [] = Nothing first (x:_) = Just x instance HasFirst ByteString where type Element ByteString = Word8 first b = b ! 0 instance Ord x => HasFirst (Set x) where type Element (Set x) = x first s = findMin s

Ahora tenemos un nuevo problema, sin embargo. Considere intentar "corregir" el Functor para que funcione con todos los tipos de contenedores:

class Functor f where type Element f :: * fmap :: (Functor f2) => (Element f -> Element f2) -> f -> f2

Esto no funciona en absoluto. Dice que si tenemos una función desde el tipo de elemento f hasta el tipo de elemento f2 , entonces podemos convertir una f en un f2 . Hasta ahora tan bueno. Sin embargo, aparentemente no hay forma de exigir que f y f2 sean del mismo tipo de contenedor.

Bajo la definición de Functor existente, tenemos

fmap :: (x -> y) -> [x] -> [y] fmap :: (x -> y) -> Seq x -> Seq y fmap :: (x -> y) -> IO x -> IO y

Pero no tenemos fmap :: (x -> y) -> IO x -> [y] . Eso es bastante imposible. Pero la definición de clase anterior lo permite.

¿Alguien sabe cómo explicar al sistema de tipos lo que realmente quería decir?

Editar

Lo anterior funciona al definir una forma de calcular un tipo de elemento a partir de un tipo de contenedor. ¿Qué pasa si intentas hacerlo al revés? ¿Definir una función para calcular un tipo de contenedor a partir de un tipo de elemento? ¿Eso funciona más fácil?


Bueno, el problema es que no está claro qué se supone que significa el Functor revisado. Por ejemplo, considere ByteString . Un ByteString solo se puede asignar al reemplazar cada elemento de Word8 con un elemento del mismo tipo . Pero Functor es para estructuras paramétricas mapeables. Realmente hay dos nociones conflictivas de mapeo aquí:

  • Mapeo rígido (es decir, transformar cada elemento de una estructura sin cambiar su tipo)
  • Mapeo paramétrico (es decir, transformando cada elemento de una estructura a cualquier tipo)

Entonces, en este caso, no puedes explicar al sistema de tipos lo que quisiste decir, porque no tiene mucho sentido. Puedes, sin embargo, cambiar lo que quieres decir :)

El mapeo rígido es fácil de expresar con familias de tipos:

class RigidMap f where type Element f :: * rigidMap :: (Element f -> Element f) -> f -> f

En cuanto al mapeo paramétrico, hay múltiples formas de hacerlo. La forma más sencilla sería conservar el Functor actual como está. Juntas, estas clases cubren estructuras como ByteString , [] , Seq , etc. Sin embargo, todos caen en Set y Map , debido a la restricción Ord de los valores. Afortunadamente, la extensión de tipos de restricciones que viene en GHC 7.4 nos permite solucionar este problema:

class RFunctor f where type Element f a :: Constraint type Element f a = () -- default empty constraint fmap :: (Element f a, Element f b) => (a -> b) -> f a -> f b

Aquí, estamos diciendo que cada instancia debe tener una restricción de clase de tipo asociada. Por ejemplo, la instancia de Set tendrá el Element Set a = Ord a , para indicar que los Set s solo se pueden construir si una instancia de Ord está disponible para el tipo. Cualquier cosa que pueda aparecer en la mano izquierda de => es aceptada. Podemos definir nuestras instancias anteriores exactamente como eran, pero también podemos hacer Set y Map :

instance RFunctor Set where type Element Set a = Ord a fmap = Set.map instance RFunctor Map where type Element Map a = Ord a fmap = Map.map

Sin embargo, es bastante molesto tener que usar dos interfaces separadas para la asignación rígida y la asignación paramétrica restringida. De hecho, ¿no es esta última una generalización de la primera? Considere la diferencia entre Set , que solo puede contener instancias de Ord , y ByteString , que solo puede contener Word8 s. ¿Seguramente podemos expresarlo como otra restricción?

Aplicamos el mismo truco hecho a HasFirst (es decir, damos instancias para toda la estructura y usamos una familia de tipos para exponer el tipo de elemento), e introducimos una nueva familia de restricciones asociada:

class Mappable f where type Element f :: * type Result f a r :: Constraint map :: (Result f a r) => (Element f -> a) -> f -> r

La idea aquí es que el Result far expresa en gran Result far las restricciones que necesita en el tipo de valor (como Ord a ), y también restringe el tipo de contenedor resultante como sea necesario; presumiblemente, para asegurarse de que tenga el tipo de un mismo tipo de contenedor de a s. Por ejemplo, el Result [a] br probablemente requerirá que r sea [b] , y el Result ByteString br requerirá que b sea Word8 , r sea ByteString .

Las familias de tipos ya nos dan lo que necesitamos para expresar "es" aquí: una restricción de igualdad de tipos. Podemos decir (a ~ b) => ... para exigir que a y b sean del mismo tipo. Podemos, por supuesto, usar esto en las definiciones de familias de restricción. Entonces, tenemos todo lo que necesitamos; a las instancias:

instance Mappable [a] where type Element [a] = a type Result [a] b r = r ~ [b] -- The type in this case works out as: -- map :: (r ~ [b]) => (a -> b) -> [a] -> r -- which simplifies to: -- map :: (a -> b) -> [a] -> [b] map = Prelude.map instance Mappable ByteString where type Element ByteString = Word8 type Result ByteString a r = (a ~ Word8, r ~ ByteString) -- The type is: -- map :: (b ~ Word8, r ~ ByteString) => (Word8 -> b) -> ByteString -> r -- which simplifies to: -- map :: (Word8 -> Word8) -> ByteString -> ByteString map = ByteString.map instance (Ord a) => Mappable (Set a) where type Element (Set a) = a type Result (Set a) b r = (Ord b, r ~ Set b) -- The type is: -- map :: (Ord a, Ord b, r ~ Set b) => (a -> b) -> Set a -> r -- (note the (Ord a) constraint from the instance head) -- which simplifies to: -- map :: (Ord a, Ord b) => (a -> b) -> Set a -> Set b map = Set.map

¡Perfecto! Podemos definir instancias para cualquier tipo de contenedor que queramos, rígido, paramétrico o paramétrico, pero restringido, y los tipos funcionan perfectamente.

Descargo de responsabilidad: no he probado GHC 7.4 todavía, así que no sé si algo de esto realmente compila o funciona, pero creo que las ideas básicas son sólidas.