I am a long time monad transformer user, first time monad transformer writer.... And I feel like I've done something unnecessary.
We are working on a project that has multiple DB tables, and hardcoding the set into different monad stacks was becoming unwieldy, so we decided to break it into different pluggable monad transformers, allowing us to pick and choose at the function type level, like this
doSomething::(HasUserTable m, HasProductTable m)=>Int->m String
(HasXTable is the class, XTableT is the concrete monad transformer). These separate monad transformers could be inserted or removed in a fully modular fashion, and would store the DB handles, require ResourceT, etc....
My first attempt was to just wrap around ReaderT, which would be used to hold the DB handle. It became immediately apparent that this would not work, as ReaderT (and StateT, etc) can not be stacked without using chains of hardcoded "lift"s, thus breaking the pluggable modularity of the stack elements.
The only solution seemed to be to write completely separate copies of the ReaderT monad, each allowing access to the others at a lower level. This works, but the solution is filled with boilerplate code, something like this
class HasUserTable m where
getUser::String->m User
newtype UserTableT m r = UserTableT{runUserTableT::String->m r}
--Standard monad instance stuff, biolerplate copy of ReaderT
instance Functor m=>Functor (UserTableT m) where....
instance Applicative m=>Applicative (UserTableT m) where....
instance Monad m=>Monad (UserTableT m) where....
instance Monad m=>HasUserTable (UserTableT m) where....
--Gotta hardcode passthrough rules to every other monad transformer
--in the world, mostly using "lift"....
instance MonadTrans BlockCacheT where....
instance (HasUserTable m, Monad m)=>HasUserTable (StateT a m)....
instance (HasUserTable m, Monad m)=>HasUserTable (ResourceT m)....
.... etc for all other monad transformers
--Similarly, need to hardcode passthrough rules for all other monads
--through the newly created one
instance MonadResource m=>MonadResource (UserTableT m) where....
instance MonadState a m=>MonadState a (UserTableT m) where....
instance (MonadBaseControl IO m) => MonadBaseControl IO (UserTableT m)....
.... etc for all other monad transformers
What makes this even worse is that we need to add even more passthrough rules for each new monad transformer we add (ie- each new table we add needs to passthrough all the other table monad transformers, so we need n^2 instance declarations!)
Is there a cleaner way to do this?
ReaderT String m r
instead, you can use generalized newtype deriving to derive those instances which are identical to the ones for reader (which seems like most of them here). You can replace most of the instances withMonadTrans t, HasUserTable m => HasUserTable (t m)
but this sort of kills type inference, and requires several extensions. – user2407038MonadTrans t, HasUserTable m=>HasUserTable (t m)
was that it applied to UserTableT also, conflicting with the proper instance that I needed to write. I suspect that this is why the n^2 problem exists at all (otherwise they would have just done this for all monad transformers). I think your comment on the n^2 problem might be the answer to my question, although not a happy one.... You can't do anything better with monad transformers, and perhaps even Haskell. I you have a reference discussing this problem, I'd accept that as the answer. – jamshidh