If you are willing to slightly modify the original interpreter (by changing how the terminal case is handled)
{-# 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
...then there's a way to add extra actions without modifying the functor or having to repurpose its constructors, while still being able to reuse the interpreter.
The solution uses the pipes
and mmorph
packages.
First you have to define a sort of "pre-interpeter" that lifts the free monad into a Producer
from pipes
. The yield ()
statements in the producer signify the points at which an extra action is inserted.
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
(In a more complex example the yield
statements could carry extra information, like log messages.)
Then you write a function that applies the printFoo
interpreter underneath the Producer
, using hoist
from mmorph
:
printFooUnder :: Producer () (Free FooF) a -> Producer () IO a
printFooUnder = hoist printFoo
So, we have a function that "interprets" the free monad into IO
, but at some points emits ()
values that we must decide how to handle.
Now we can define an extended interpreter that reuses the old interpreter:
printFooWithReuse :: Show a => Free FooF a -> IO ()
printFooWithReuse foo = do
finalv <- runEffect $ for (printFooUnder . pre $ foo)
(\_ -> lift (print "extra info"))
print finalv
After testing it, it seems to work:
printFooWithReuse $ Free (Foo "nah" (Pure 4))
-- > "nah"
-- > "extra info"
-- > 4
If you happen to want to insert the extra actions manually, then your can eschew writing the "pre-interpreter" and work directly in the Producer () (Free FooF)
monad.
(This solution could also be achieved by layering a free monad transformer instead of a Producer
. But I think using a Producer
is a bit easier.)