haskell - monadas - ¿Es posible extender intérpretes de mónada gratis?
monadas filosofia (5)
¿Estás buscando algo como esto?
Tu código original sería
{-# LANGUAGE DeriveFunctor #-}
import Control.Monad.Free
data FooF a = Foo String a | Bar Int a deriving (Functor)
type Foo = Free FooF
printFoo :: Show a => Foo a -> IO ()
printFoo (Free (Foo s n)) = print s >> printFoo n
printFoo (Free (Bar i n)) = print i >> printFoo n
printFoo (Pure a) = print a
Luego, puede definir una función de envoltura simple y un anotador recursivo que agrega la información adicional a cada capa de Foo
(obviamente, esto puede ser tan complicado como desee).
annotate :: Foo a -> Foo a
annotate (Free (Foo s n)) = wrapper (Free (Foo s (annotate n)))
annotate (Free (Bar i n)) = wrapper (Free (Bar i (annotate n)))
annotate (Pure a) = wrapper (Pure a)
wrapper :: Foo a -> Foo a
wrapper n = Free (Foo "Extra info" n)
Ahora define algunos constructores de conveniencia que definen tu DSL
foo :: String -> a -> Foo a
foo s a = Free (Foo s (Pure a))
bar :: Int -> a -> Foo a
bar i a = Free (Bar i (Pure a))
Lo que significa que puedes crear objetos Foo a
a usando la interfaz de mónada y tu DSL.
example = do
i <- return 1
a <- foo "Created A" i
b <- bar 123 a
c <- foo "Created C" b
return c
Ahora, si carga GHCI, puede trabajar con el example
original o con la versión anotada.
>> printFoo example
"Created A"
123
"Created C"
1
>> printFoo (annotate example)
"Extra info"
"Created A"
"Extra info"
123
"Extra info"
"Created C"
"Extra info"
1
Dada una mónada DSL gratuita como:
data FooF x = Foo String x
| Bar Int x
deriving (Functor)
type Foo = Free FooF
Y un intérprete al azar para Foo
:
printFoo :: Foo -> IO ()
printFoo (Free (Foo s n)) = print s >> printFoo n
printFoo (Free (Bar i n)) = print i >> printFoo n
Me parece que debería ser posible intercalar algo en cada iteración de printFoo sin tener que hacerlo manualmente:
printFoo'' :: Foo -> IO ()
printFoo'' (Free (Foo s n)) = print s >> print "extra info" >> printFoo'' n
printFoo'' (Free (Bar i n)) = print i >> print "extra info" >> printFoo'' n
¿Es esto posible de alguna manera ''envolviendo'' el printFoo
original?
Motivación: Estoy escribiendo un DSL pequeño que se ''compila'' en un formato binario. El formato binario contiene información adicional después de cada comando de usuario. Tiene que estar allí, pero es totalmente irrelevante en mi caso de uso.
Ambas cosas simplemente atraviesan la estructura y acumulan el resultado del procesamiento inductivo. Esto requiere generalizar la iteración a través del catamorfismo.
> newtype Fix f = Fix {unFix :: f (Fix f)}
> data N a b x = Z a | S b x deriving (Functor)
> type Nat a b = Fix (N a b)
> let z = Fix . Z
> let s x = Fix . S x
> let x = s "blah" $ s "doo" $ s "duh" $ z 0
> let annotate (Z x) = s "annotate" $ z x;
annotate (S x y) = s "annotate" $ s x y
> let exec (Z x) = print x; exec (S x y) = print x >> y
> let cata phi = phi . fmap (cata phi) . unFix
>
> cata exec x
"blah"
"doo"
"duh"
0
>
> cata exec $ cata annotate x
"annotate"
"blah"
"annotate"
"doo"
"annotate"
"duh"
"annotate"
0
Ahora permítanme explicar con más detalle lo que está sucediendo, ya que hubo algunos pedidos en los comentarios, y me preocupa que ya no sea una mónada, si uso Fix.
Considere el functor G:
G(X) = A + F(G(X))
Aquí F es un functor arbitrario. Entonces para cualquier A podemos encontrar un punto fijo (F y G son claramente polinomiales, estamos en Hask). Como asignamos cada objeto A de la categoría a un objeto de la categoría, estamos hablando de un funtor de puntos fijos, T (A). Resulta que es una mónada. Como es una mónada para cualquier functor F, T (A) es una mónada libre. (Verás que es obviamente una mónada del código de abajo)
{-# LANGUAGE DeriveFunctor
, TypeSynonymInstances #-}
newtype Fix f = Fix {unFix :: f (Fix f)} -- the type of Fixed point of a functor
newtype Compo f g x = Compo {unCompo :: f (g x)} -- composition of functors
instance (Functor f, Functor g) => Functor (Compo f g) where -- composition of functors is a functor
fmap f = Compo . fmap (fmap f) . unCompo
data FreeF a x = Pure a | Free x deriving (Functor) -- it is a bi-functor, really;
-- this derives functor in x
-- a special case of fmap - the fmap with unwrapping; useful to eliminate pattern matching
ffmap :: (a -> b) -> FreeF b a -> b
ffmap f x = case fmap f x of -- unwrapping, since now distinction between Pure and Free is not important
Pure a -> a
Free a -> a
-- Free Monad is a functor of fixed points of functor G(X)
-- G(X) = A + F(G(X))
type Free f a = Fix (Compo (FreeF a) f) -- fixed point of composition F . (FreeF a)
-- unfortunately, when defined as type, (Free f a) cannot be declared
-- as a Monad (Free f) - Haskell wants Free f to be with `a`
-- instance Monad (Free f) where -- this derives a functor in a at the same time;
-- note that fmap will work in x, and is not meant
-- to be equal to (m >>= return . f), which is in `a`
-- return a = Fix $ Compo $ Pure a
-- (Fix (Compo (Pure a))) >>= f = f a
-- (Fix (Compo (Free fx))) >>= f = Fix $ Compo $ Free $ fmap (>>= f) fx
ret :: (Functor f) => a -> Free f a -- yet it is a monad: this is return
ret = Fix . Compo . Pure
-- and this is >>= of the monad
bind :: (Functor f) => Free f a -> (a -> Free f b) -> Free f b
bind (Fix (Compo (Pure a))) f = f a
bind (Fix (Compo (Free fx))) f = Fix $ Compo $ Free $ fmap (`bind` f) fx
-- Free is done
-- here is your functor FooF
data FooF x = Z Int x | S String x deriving (Functor)
type Foo x = Free FooF x
-- catamorphism for an algebra phi "folds" any F(X) (represented by fixed point of F)
-- into X
cata :: (Functor f) => (f x -> x) -> Fix f -> x
cata phi = phi . fmap (cata phi) . unFix
-- helper functions to construct "Foo a"
z :: Int -> Foo a -> Foo a
z x = Fix . Compo . Free . Z x
s :: String -> Foo a -> Foo a
s x = Fix . Compo . Free . S x
tip :: a -> Foo a
tip = ret
program :: Foo (IO ())
program = s "blah" $ s "doo" $ s "duh" $ z 0 $ tip $ return ()
-- This is essentially a catamorphism; I only added a bit of unwrapping
cata'' :: (Functor f) => (f a -> a) -> Free f a -> a
cata'' phi = ffmap (phi . fmap (cata'' phi)) . unCompo . unFix
exec (Z x y) = print x >> y
exec (S x y) = print x >> y
annotate (Z x y) = s "annotated Z" $ z x y
annotate (S x y) = s "met S" $ s x y
main = do
cata'' exec program
cata'' exec $ cata'' annotate (program `bind` (ret . ret))
-- cata'' annotate (program >>= return . return)
-- or rather cata'' annotate $ fmap return program
program
es Foo (IO ())
. fmap
en a
(recuerde que FreeF es un bi-functor: necesitamos fmap en a
) puede convertir el program
en Foo (Foo (IO ()))
- ahora el catamorfismo para anotado puede construir un nuevo Foo (IO ())
.
Tenga en cuenta que cata''
es iter
de Control.Monad.Free
.
Aquí hay una solución muy simple que utiliza el paquete operational
: la alternativa razonable a las mónadas gratuitas.
Simplemente puede factorizar la función printFoo
en una parte que imprime la instrucción propiamente dicha y una parte que agrega la información adicional, el tratamiento estándar para la duplicación de códigos como este.
{-# LANGUAGE GADTs #-}
import Control.Monad.Operational
data FooI a where
Foo :: String -> FooI ()
Bar :: Int -> FooI ()
type Foo = Program FooI
printFoo :: Foo a -> IO a
printFoo = interpretWithMonad printExtra
where
printExtra :: FooI a -> IO a
printExtra instr = do { a <- execFooI instr; print "extra info"; return a; }
execFooI :: FooI a -> IO a
execFooI (Foo s) = print s
execFooI (Bar i) = print i
Las otras respuestas han pasado por alto lo simple que hace esto free
! :) Actualmente tienes
{-# LANGUAGE DeriveFunctor #-}
import Control.Monad.Free
data FooF x = Foo String x
| Bar Int x
deriving (Functor)
type Foo = Free FooF
program :: Free FooF ()
program = do
liftF (Foo "Hello" ())
liftF (Bar 1 ())
liftF (Foo "Bye" ())
printFoo :: Foo () -> IO ()
printFoo (Free (Foo s n)) = print s >> printFoo n
printFoo (Free (Bar i n)) = print i >> printFoo n
printFoo (Pure a) = return a
lo que da
*Main> printFoo program
"Hello"
1
"Bye"
Eso está bien, pero iterM
puede hacer la tubería necesaria para usted
printFooF :: FooF (IO a) -> IO a
printFooF (Foo s x) = print s >> x
printFooF (Bar i x) = print i >> x
printFooBetter :: Foo () -> IO ()
printFooBetter = iterM printFooF
Entonces conseguimos
*Main> printFooBetter program
"Hello"
1
"Bye"
OK genial, es lo mismo que antes. Pero printFooF
nos da más flexibilidad para aumentar el traductor en las líneas que desee
printFooFExtra :: FooF (IO a) -> IO a
printFooFExtra = (print "stuff before IO action" >>)
. printFooF
. fmap (print "stuff after IO action" >>)
printFooExtra :: Foo () -> IO ()
printFooExtra = iterM printFooFExtra
entonces obtenemos
*Main> printFooExtra program
"stuff before IO action"
"Hello"
"stuff after IO action"
"stuff before IO action"
1
"stuff after IO action"
"stuff before IO action"
"Bye"
"stuff after IO action"
¡Gracias Gabriel González por popularizar las mónadas gratuitas y Edward Kmett por escribir la biblioteca! :)
Si está dispuesto a modificar ligeramente el intérprete original (cambiando la forma en que se maneja el caso del terminal)
{-# LANGUAGE DeriveFunctor #-}
import Control.Monad.Free
import Control.Monad.Morph
import Pipes
data FooF a = Foo String a | Bar Int a deriving (Functor)
printFoo :: Free FooF a -> IO a
printFoo (Free (Foo s n)) = print s >> printFoo n
printFoo (Free (Bar i n)) = print i >> printFoo n
printFoo (Pure a) = return a
... luego hay una manera de agregar acciones adicionales sin modificar el funtor o tener que reutilizar a sus constructores, mientras se puede reutilizar el intérprete.
La solución utiliza las pipes
y los paquetes mmorph
.
Primero tienes que definir una especie de "pre-intérprete" que levanta la mónada libre a un Producer
desde las pipes
. Las declaraciones de yield ()
en el productor representan los puntos en los que se inserta una acción adicional.
pre :: Free FooF a -> Producer () (Free FooF) a
pre (Free (Foo s n)) = lift (Free . Foo s $ return ()) >> yield () >> pre n
pre (Free (Bar i n)) = lift (Free . Bar i $ return ()) >> yield () >> pre n
pre (Pure a) = lift . Pure $ a
(En un ejemplo más complejo, las declaraciones de yield
podrían llevar información adicional, como los mensajes de registro).
Luego escribe una función que aplica el intérprete de printFoo
debajo del Producer
, utilizando hoist
from mmorph
:
printFooUnder :: Producer () (Free FooF) a -> Producer () IO a
printFooUnder = hoist printFoo
Entonces, tenemos una función que "interpreta" la mónada libre en IO
, pero en algunos puntos emite ()
valores que debemos decidir cómo manejarlos.
Ahora podemos definir un intérprete extendido que reutiliza el intérprete antiguo:
printFooWithReuse :: Show a => Free FooF a -> IO ()
printFooWithReuse foo = do
finalv <- runEffect $ for (printFooUnder . pre $ foo)
(/_ -> lift (print "extra info"))
print finalv
Después de probarlo, parece funcionar:
printFooWithReuse $ Free (Foo "nah" (Pure 4))
-- > "nah"
-- > "extra info"
-- > 4
Si desea insertar las acciones adicionales manualmente, puede evitar escribir el "pre-intérprete" y trabajar directamente en la mónada Producer () (Free FooF)
.
(Esta solución también podría lograrse mediante la colocación en capas de un transformador de mónada libre en lugar de un Producer
. Pero creo que usar un Producer
es un poco más fácil).