Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Monad Stack Penetration Classes with Free/Operational Monad Transformers?

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 ProgramTs 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 :)

like image 710
nushio Avatar asked Jul 30 '13 01:07

nushio


1 Answers

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.

like image 53
Petr Avatar answered Oct 19 '22 20:10

Petr