How can I catch exceptions in this monad stack?

Viewed 107

I have the following monad stack:

import Control.Monad.Trans.State ( runStateT, StateT )
import Control.Monad.Trans.Except ( catchE, throwE, runExcept, Except )

type MyMonad a = StateT Int (StateT Char (Except String)) a

runMyMonad :: MyMonad a -> Char -> Int -> Either Error ((a, Int), Char)
runMyMonad f c i = runExcept $ runStateT (runStateT f i) c

If I want to throw an error in an action, I can do this:

-- Takes an Int and returns its double. 
-- Throws an error if it is not larger than 5.
myAction :: Int -> MyMonad Int
myAction a = 
    if a > 5 
        then return $ a*2 
        else lift . lift $ throwE "There was an error"

That works, but I can't catch that error in another action like this:

-- Returns 0.
myBaseAction :: MyMonad Int
myBaseAction = return 0

-- Takes an Int and returns its double. 
-- Returns 0 if it is not larger than 5.
mySafeAction :: Int -> MyMonad Int
mySafeAction a = lift . lift $ catchE (myAction a) (\e -> myBaseAction)

Haskell complains about the types of the arguments of catchE, saying that it expected an ExceptT but I gave it a MyMonad. How can I make catchE take MyMonad? I've read about the liftCatch function in some posts, but I can't find it anywhere.

2 Answers

The usual way to do this is to use throwError and catchError from the mtl library. Appropriate instances of the associated class have already been written for all the transformers defined in the transformers library. So, that would look like this:

import Control.Monad.State
import Control.Monad.Except

myAction :: Int -> MyMonad Int
myAction a = 
    if a > 5 
        then return $ a*2 
        else throwError "There was an error"

mySafeAction :: Int -> MyMonad Int
mySafeAction a = catchError (myAction a) (\e -> return 0)

In fact, it's even quite normal to characterize your monad stack by the effects used in a particular action:

myAction' :: MonadError String m => Int -> m Int
myAction' = {- same as before -}

mySafeAction' :: MonadError String m => Int -> m Int
mySafeAction' = {- same as before -}

This can be specialized to m ~ MyMonad at the top level of your application.

...but you will have a sad surprise when you go to use MonadState in this way with your transformer! See effect-stack for one approach at dealing with that problem.

I will answer this since I ended up finding a solution, and I couldn't find any other answers.

The function I had to use for lifting was liftCatch from Control.Monad.Trans.State. The code now looks like this:

import Control.Monad.Trans.State ( liftCatch )

-- Takes an Int and returns its double. 
-- Returns 0 if it is not larger than 5.
mySafeAction :: Int -> MyMonad Int
mySafeAction a = (liftCatch $ liftCatch catchE) (myAction a) (\e -> myBaseAction)

Edit: this is the way to lift it manually, but Daniel's solution above is a lot cleaner

Related