This is going to be a long one because I’m not sure that I went into this in the right frame of mind, so I’m going to outline my thinking as clearly as possible at each step of the way. I’ve got two code snippets that are about as minimal as I can make them, so feel free to use those.
I started with a single transformer FitStateT m a, which just holds the state of the program at the time and allowed saving to disk:
data FitState = FitState
newtype FitStateT m a = FitStateT (StateT FitState m a) deriving (Monad, MonadTrans)
At some point further into the project I decided to add haskeline into the project which has some datatypes likes this:
-- Stuff from haskeline. MonadException is something that haskeline requires for whatever reason.
class MonadIO m => MonadException m
newtype InputT m a = InputT (m a) deriving (Monad, MonadIO)
So my routines in my main file would look something like this:
myMainRoutineFunc :: (MonadException m, MonadIO m) => FitStateT (InputT m) ()
myMainRoutineFunc = do
myFitStateFunc
lift $ myInputFunc
return ()
Unfortunately there were a number of problems with this as my program grew. The main problem was that for every input function I ran, I had to lift before I ran it. The other problem is for every function that ran an input command, I required a MonadException m constraint on it. Also for any function that ran a fitstate related function it required a MonadIO m constraint on it.
Here’s the code: https://gist.github.com/4364920
So I decided to create some classes to make this fit together a little better and to clean up the types a little. My goal is to be able to write something like this:
myMainRoutineFunc :: (MonadInput t m, MonadFitState t m) => t m ()
myMainRoutineFunc = do
myFitStateFunc
myInputFunc
return ()
First I created a MonadInput class to wrap around the InputT type and then my own routine would just become an instance of this class.
-- Stuff from haskeline. MonadException is something that haskeline requires for whatever reason.
class MonadIO m => MonadException m
newtype InputT m a = InputT (m a) deriving (Monad, MonadIO)
-- So I add a new class MonadInput
class MonadException m => MonadInput t m where
liftInput :: InputT m a -> t m a
instance MonadException m => MonadInput InputT m where
liftInput = id
I added the MonadException constraint so that I would not have to specify it separately on every input related function. This necessitated adding multiparamtypeclasses and flexibleinstances, but the resulting code was exactly what I was looking for:
myInputFunc :: MonadInput t m => t m (Maybe String)
myInputFunc = liftInput $ undefined
So then I did the same for FitState. Again I added the MonadIO constraint:
-- Stuff from my own transformer. This requires that m be MonadIO because it needs to store state to disk
data FitState = FitState
newtype FitStateT m a = FitStateT (StateT FitState m a) deriving (Monad, MonadTrans, MonadIO)
class MonadIO m => MonadFitState t m where
liftFitState :: FitStateT m a -> t m a
instance MonadIO m => MonadFitState FitStateT m where
liftFitState = id
Which again works perfectly.
myFitStateFunc :: MonadFitState t m => t m ()
myFitStateFunc = liftFitState $ undefined
And then I wrapped my main routine into a newtype wrapper so that I could create instances of these two classes:
newtype Routine m a = Routine (FitStateT (InputT m) a)
deriving (Monad, MonadIO)
And then an instance of MonadInput:
instance MonadException m => MonadInput Routine m where
liftInput = Routine . lift
Works perfectly. Now for MonadFitState:
instance MonadIO m => MonadFitState Routine m where
liftFitState = undefined
-- liftFitState = Routine -- This fails with an error.
Ah crap, it fails.
Couldn't match type `m' with `InputT m'
`m' is a rigid type variable bound by
the instance declaration at Stack2.hs:43:18
Expected type: FitStateT m a -> Routine m a
Actual type: FitStateT (InputT m) a -> Routine m a
In the expression: Routine
In an equation for `liftFitState': liftFitState = Routine
And I don’t know what to do to make this work. I don’t really understand the error. Does it mean I have to make FitStateT an instance of MonadInput? That seems really weird, those are two completely different modules with nothing in common. Any help would be appreciated. Is there a better way to get what I’m looking for?
Completed code with error: https://gist.github.com/4365046
Well, to start with, here’s the type of
liftFitState:And here’s the type of
Routine:Your
liftFitStatefunction expects a single wrapper type to be converted fromFitStateT, butRoutinehas two layers of transformer it wraps. So the types won’t match up.Beyond that, I really suspect you’re going about this the wrong way.
First of all, if you’re writing an application, not a library, it’s more common to simply wrap all the monad transformers you need in one big stack and use that everywhere. Typically the only reason for leaving it as a transformer would be for switching between a limited number of base monads, e.g.
Identity,IO,ST, orSTM. But even that’s overkill if everything you need your transformer stack for requires IO and you’re not intending to useSTorSTM.In your case the simplest approach would apparently look something like this:
…then derive or manually implement the
MonadFooclasses you want (e.g.MonadIO), and simply use that stack everywhere.The benefit of doing it this way, instead of mucking about with multiple layers, is that later on if you need to add another transformer the way you added Haskeline–deciding to add a
ReaderTfor some sort of global data resources, say–you can simply add it to the wrapped stack and all the code currently using the stack won’t even know the difference.On the other hand, if you really do want to take your current approach, you’re getting the monad transformer lifting idiom a bit wrong. The basic lifting operation should come from
MonadTrans, which you’re already deriving.MonadFooclasses are generally for providing the essential operations for each monad generically, e.g.getandputforMonadState.You seem to be trying to imitate
liftIO, which is a “lift all the way” operation toliftenough times to get from the bottom of the stack–IO–to the actual monad. This doesn’t really make sense for transformers that can appear anywhere in a stack.If you do want to have your own
MonadFooclasses, I suggest looking at the source for classes likeMonadStateand seeing how they work, then following the same pattern.