haskell boilerplate

¿Cómo evito escribir este tipo de código de caldera Haskell?



boilerplate (4)

Hay dos soluciones simples principales para esto.

Primero, para tipos simples, simplemente deriving (Functor) usando la extensión necesaria.

La otra solución es definir otro tipo de datos:

data Bar = S String | B Bool | I Int -- "Inner" type data Foo a = X a | Q Bar -- "Outer" type instance Functor Foo where fmap f (X a) = X (f a) fmap _ (Q b) = Q b -- `b'' requires no type change.

Así que puedes escribir una línea más para eliminar muchas.

No es exactamente ideal para la coincidencia de patrones, pero al menos resuelve este problema.

Me encuentro con esta situación con la frecuencia suficiente para que sea molesto.

Digamos que tengo un tipo de suma que puede contener una instancia de x o un montón de otras cosas no relacionadas con x -

data Foo x = X x | Y Int | Z String | ...(other constructors not involving x)

Para declarar una instancia de Functor tengo que hacer esto -

instance Functor Foo where fmap f (X x) = X (f x) fmap _ (Y y) = Y y fmap _ (Z z) = Z z ... And so on

Mientras que lo que me gustaría hacer es esto -

instance Functor Foo where fmap f (X x) = X (f x) fmap _ a = a

es decir, solo me importa el constructor X , todos los demás constructores simplemente se "pasan". Pero, por supuesto, esto no se compilaría porque a en el lado izquierdo es un tipo diferente del a en el lado derecho de la ecuación.

¿Hay alguna manera de evitar escribir esta placa para los otros constructores?


Parece un trabajo para prismas.

Descargo de responsabilidad: Soy un principiante de lentes / prisma.

{-# LANGUAGE TemplateHaskell #-} import Control.Lens import Control.Lens.Prism data Foo x = X x | Y Int | Z String deriving Show makePrisms ''''Foo instance Functor Foo where -- super simple impl, by András Kovács fmap = over _X -- My overly complicated idea -- fmap f = id & outside _X .~ (X . f) -- Original still more complicated implementation below -- fmap f (X x) = X (f x) -- fmap _ a = id & outside _X .~ undefined $ a

Uso:

*Main> fmap (++ "foo") (Y 3) Y 3 *Main> fmap (++ "foo") (X "abc") X "abcfoo"


Principalmente para completar, aquí hay una manera de hacerlo:

import Unsafe.Coerce instance Functor Foo where fmap f (X x) = X (f x) fmap _ a = unsafeCoerce a

En la situación descrita por usted, este sería un uso seguro de unsafeCoere . Pero hay buenas razones para evitar esto:

  • La seguridad depende de cómo GHC compila las estructuras de datos y el código; Saber que el programador normal no debería tener que tener.
  • Tampoco es robusto: si el tipo de datos se amplía con un nuevo constructor X ''x, no se generará ninguna advertencia porque el catch-all hace que esta definición sea exhaustiva y luego todo irá. (Gracias @gallais por ese comentario)

Por lo tanto, esta solución definitivamente no es aconsejable.


Supongo que nos gustaría tener una solución para el caso general donde el parámetro de tipo de cambio no está necesariamente en la posición correcta para DeriveFunctor .

Podemos distinguir dos casos.

En el caso simple, el tipo de datos no es recursivo. Aquí, los prisms son una solución adecuada:

{-# LANGUAGE TemplateHaskell #-} import Control.Lens data Foo x y = X x | Y y | Z String makePrisms ''''Foo mapOverX :: (x -> x'') -> Foo x y -> Foo x'' y mapOverX = over _X

Si nuestros datos son recursivos, entonces las cosas se complican más. Ahora makePrisms no crea prismas que cambian de tipo. Podemos deshacernos de la recursión en la definición factorizándola hasta un punto de referencia explícito. De esta manera nuestros prismas se mantienen cambiantes:

import Control.Lens newtype Fix f = Fix {out :: f (Fix f)} -- k marks the recursive positions -- so the original type would be "data Foo x y = ... | Two (Foo x y) (Foo x y)" data FooF x y k = X x | Y y | Z String | Two k k deriving (Functor) type Foo x y = Fix (FooF x y) makePrisms ''''FooF mapOverX :: (x -> x'') -> Foo x y -> Foo x'' y mapOverX f = Fix . -- rewrap over _X f . -- map f over X if possible fmap (mapOverX f) . -- map over recursively out -- unwrap

O podemos factorizar la transformación de abajo a arriba:

cata :: (Functor f) => (f a -> a) -> Fix f -> a cata f = go where go = f . fmap go . out mapOverX :: (x -> x'') -> Foo x y -> Foo x'' y mapOverX f = cata (Fix . over _X f)

Hay una literatura considerable sobre el uso de puntos de corrección de los funtores para la programación genérica, y también una serie de bibliotecas, por ejemplo, this o this . Es posible que desee buscar "esquemas de recursión" para obtener más referencias.