Can there be mtl-like mechanism for monad transformers created by FreeT / ProgramT ?
My understanding of the history is as follows. Once upon a time monad transformer was invented. Then people started to stack monad transformers one on other, then found it annoying to insert lift
everywhere. Then a couple of people invented monad classes, so that we can e.g. ask :: m r
in any monad m
such that MonadReader r m
. This was possible by making every monad class penetrate every monad transformer, like
(Monoid w, MonadState s m) => MonadState s (WriterT w m)
MonadWriter w m => MonadWriter w (StateT s m)
you need such pair of instance declarations for every pair of monad transformers, so when there's n monad transformers there's n^2 costs. This was not a large problem, however, because people will mostly use predefined monads and rarely create their own. The story so far I understand, and also is detailed e.g. in the following Q&A:
Avoiding lift with Monad Transformers
Then my problem is with the new Free monads http://hackage.haskell.org/package/free and Operational monads http://hackage.haskell.org/package/operational . They allow us to write our own DSL and use it as monads, just by defining the language as some algebraic data
type (Operational doesn't even need Functor
instances). Good news is that we can have monads and monad transformers for free; then how about monad classes? Bad news is that the assumption "we rarely define our own monad transformers" no longer holds.
As an attempt to understand this problem, I made two ProgramT
s and made them penetrate each other;
https://github.com/nushio3/practice/blob/master/operational/exe-src/test-05.hs
The operational
package does not support monad classes so I took another implementation minioperational
and modified it to work as I need; https://github.com/nushio3/minioperational
Still, I needed the specialized instance declaration
instance (Monad m, Operational ILang m) => Operational ILang (ProgramT SLang m) where
because the general declaration of the following form leads to undecidable instances.
instance (Monad m, Operational f m) => Operational f (ProgramT g m) where
My question is that how can we make it easier to let our Operational monads penetrate each other. Or, is my wish to have penetration for any Operational monad ill-posed.
I'd also like to know the correct technical term for penetration :)
I tried a bit different approach, which gives at least a partial answer. Since stacking monads can be sometimes problematic, and we know all our monads are constructed from some data type, I tried instead to combine the data types.
I feel more comfortable with MonadFree
so I used it, but I suppose a similar approach could be used for Operational
as well.
Let's start with the definition of our data types:
{-# LANGUAGE DeriveFunctor, FlexibleContexts,
FlexibleInstances, FunctionalDependencies #-}
import Control.Monad
import Control.Monad.Free
data SLang x = ReadStr (String -> x) | WriteStr String x
deriving Functor
data ILang x = ReadInt (Int -> x) | WriteInt Int x
deriving Functor
In order to combine two functors together for using them in a free monad, let's define their coproduct:
data EitherF f g a = LeftF (f a) | RightF (g a)
deriving Functor
If we create a free monad over EitherF f g
, we can call the commands from both of them. In order to make this process transparent, we can use MPTC to allow conversion from each of the functor into the target one:
class Lift f g where
lift :: f a -> g a
instance Lift f f where
lift = id
instance Lift f (EitherF f g) where
lift = LeftF
instance Lift g (EitherF f g) where
lift = RightF
now we can just call lift
and convert either part into the coproduct.
With a helper function
wrapLift :: (Functor g, Lift g f, MonadFree f m) => g a -> m a
wrapLift = wrap . lift . fmap return
we can finally create generic functions that allow us to call commands from anything we can lift into a functor:
readStr :: (Lift SLang f, MonadFree f m) => m String
readStr = wrapLift $ ReadStr id
writeStr :: (Lift SLang f, MonadFree f m) => String -> m ()
writeStr x = wrapLift $ WriteStr x ()
readInt :: (Lift ILang f, MonadFree f m) => m Int
readInt = wrapLift $ ReadInt id
writeInt :: (Lift ILang f, MonadFree f m) => Int -> m ()
writeInt x = wrapLift $ WriteInt x ()
Then, the program can be expressed as
myProgram :: (Lift ILang f, Lift SLang f, MonadFree f m) => m ()
myProgram = do
str <- readStr
writeStr "Length of that str is"
writeInt $ length str
n <- readInt
writeStr "you wanna have it n times; here we go:"
writeStr $ replicate n 'H'
without defining any further instances.
While all the above works nicely, the problem is how to generically run such composed free monads. I don't know if it is even possible, to have a fully generic, composable solution.
If we have just one base functor, we can run it as
runSLang :: Free SLang x -> String -> (String, x)
runSLang = f
where
f (Pure x) s = (s, x)
f (Free (ReadStr g)) s = f (g s) s
f (Free (WriteStr s' x)) _ = f x s'
If we have two, we need to thread the state of both of them:
runBoth :: Free (EitherF SLang ILang) a -> String -> Int -> ((String, Int), a)
runBoth = f
where
f (Pure x) s i = ((s, i), x)
f (Free (LeftF (ReadStr g))) s i = f (g s) s i
f (Free (LeftF (WriteStr s' x))) _ i = f x s' i
f (Free (RightF (ReadInt g))) s i = f (g i) s i
f (Free (RightF (WriteInt i' x))) s _ = f x s i'
I guess one possibility would be to express running the functors using iter :: Functor f => (f a -> a) -> Free f a -> a
from free and then create a similar, combining function
iter2 :: (Functor f, Functor g)
=> (f a -> a) -> (g a -> a) -> Free (EitherF f g) a -> a
But I haven't had time to try it out.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With