Note that as Edward says, it would generally a lot simpler to put the ErrorT
at the top of the stack, not the bottom.
This can change the semantics of the stack, at least for more complicated transformers than ReaderT
- e.g. if you have StateT
in the stack, then with ErrorT
at the bottom changes to the state will be rolled back when there's an error, whereas with ErrorT
at the top, changes to the state will be kept when there's an error.
If you do really need it at the bottom, then something like this passes the type checker:
import Control.Monad.Error
import Control.Monad.Morph
import System.IO
toOuter :: MFunctor t => t (ErrorT String IO) a -> t IO a
toOuter = hoist runErrorTWithPrint
runErrorTWithPrint :: ErrorT String IO a -> IO a
runErrorTWithPrint m = do
res <- runErrorT m
case res of
Left err -> do
hPutStrLn stderr err
fail err
Right v -> return v
Note that it calls fail
when the inner computation fails, which isn't what your code above does.
The main reason is that to use hoist
we need to provide a function of type forall a . ErrorT String IO a -> IO a
- i.e. to handle any kind of value, not just ()
. This is because the depending on the rest of the monad stack might mean that the actual return type when you get to the ErrorT
is different to the return type you started with.
In the failure case, we don't have a value of type a
so one option is to fail.
In your original code you also loop infinitely in outer
, which this doesn't do.