Search code examples
haskellmonad-transformers

mtl, reader, exceptt & stacking order


So this is going to be a bit long, because I'm not sure how to frame this question more generally. The good news is that I have a code sample at the bottom of the question, the idea is just to make it build & elegant :-)

I have a couple of functions which have signatures like:

calledFunction
  :: (MonadReader env m, HasToken env, MonadIO m)
  => m (Either MyError MyResult)

calledFunction2
  :: (MonadReader env m, HasToken env, MonadIO m)
  => m (Either MyError MyResult2)

And I'd like to get in the end a result of type ExceptT String IO MyResult3 which I get by combining MyResult & MyResult2.

Now it's very nice that calledFunction returns an Either because I can leverage:

ExceptT :: m (Either e a) -> ExceptT e m a

And I just type EitherT calledFunction and I won't have anymore m (Either MyError MyResult) but straight ExceptT MyError m MyResult). Progress!

But I also need to give to calledFunction the reader context it wants. Now, I would do that with runReaderT. I have now come to the ExceptT MyError m MyResult transformer stack, so naturally the ReaderT should go where the m is.. So ExceptT MyError (ReaderT Config IO) MyResult...

Except, how do I 'fill in' the readerT with the value to read, since it's at the bottom of the transformer stack? And if I reverse the stack to have the reader at the toplevel, then runReaderT comes naturally, but I don't see how to use EitherT to transform my Either in an ExceptT elegantly...

import Control.Monad.Reader
import Control.Monad.Trans.Reader
import Control.Monad.Trans.Except
import Control.Monad.IO.Class
import Control.Error -- 'error' package

class HasToken a where
    getToken :: a -> String
data Config = Config String
instance HasToken Config where
    getToken (Config x) = x

data MyError = MyError String deriving Show
data MyResult = MyResult String
data MyResult2 = MyResult2 String

data MyResult3 = MyResult3 MyResult MyResult2

calledFunction
  :: (MonadReader env m, HasToken env, MonadIO m)
  => m (Either MyError MyResult)
calledFunction = undefined

calledFunction2
  :: (MonadReader env m, HasToken env, MonadIO m)
  => m (Either MyError MyResult2)
calledFunction2 = undefined

cfg = Config "test"

main = undefined

test :: ExceptT MyError IO MyResult3
test = do
    -- calling runReaderT each time defeats the purpose..
    r1 <- ExceptT (runReaderT calledFunction cfg)
    r2 <- ExceptT (runReaderT calledFunction2 cfg)
    return $ MyResult3 r1 r2

test1 = runReaderT test2 cfg

test2 :: ReaderT Config (ExceptT MyError IO) MyResult3
test2 = do
    -- how to make this compile?
    let cfg = Config "test"
    r1 <- ExceptT calledFunction
    r2 <- ExceptT calledFunction2
    return $ MyResult3 r1 r2

Solution

  • You can use hoist from Control.Monad.Morph to run the Reader below the ExceptT:

    ghci> let foo = undefined :: ExceptT () (ReaderT () IO) ()
    ghci> :t hoist (flip runReaderT ()) foo
    hoist (flip runReaderT ()) foo :: ExceptT () IO ()
    

    It's also easy to do it yourself, you just have to unwrap with runExceptT, supply the environment with runReader, and re-wrap the result in the ExceptT constructor:

    ghci> :t \env -> ExceptT . flip runReaderT env . runExceptT
    \env -> ExceptT . flip runReaderT env . runExceptT
        :: r -> ExceptT e (ReaderT r m) a -> ExceptT e m a