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
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