¿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.