Error parameterized monad and transformer, a replacement for synchronous exceptions

As of March 2020, School of Haskell has been switched to read-only mode.

Monadic computations

A Monad is an abstract data type for modelling the sequentiality of side effect capable computations that return a result value.

class Monad m where
  -- return: generates a simple computation with the parameter as result
  return :: a -> m a  

  -- (>>=) binds a monadic action with a monadic function on its result
  (>>=) :: m a -> (a -> m b) -> m b
  
  -- (>>) binds with a second computation ignoring the result of the first one
  (>>) :: m a -> m b -> m b
  
  x >> y = x >>= \_ -> y      -- (>>) can be defined in terms of (>>=)
  ...

The actual definition extends the Applicative typeclass (sequencing computations while combining its results) with return = pure and (>>) = (*>), but I want to base this guide on Monads.

The Do block is a syntax construction that translates to a Monadic composition of computations. See do notation

Failable computations. Left absorbing elements

A failable monad domain is one that has a left absorbing element in (>>=), meaning the monad composition result will be the left operand's one, stopping the evaluation of subsequent computations.

In the Either monad all the values constructed with Left are left absorbing. See Data.Either source

instance Monad (Either e) where

   Left e >>= _ = Left e          -- left absorbing, subsequent comp. are ignored
   
   Right x >>= f = f x
   
   return = Right

Exceptions

The package exceptions generalizes the exception context to more monads than IO. See Control.Monad.Catch

-- from Control.Monad.Catch

class Monad m => MonadThrow m where

  throwM :: Exception e => e -> m a
  
class MonadThrow m => MonadCatch m where  

  catch :: Exception e => m a -> (e -> m a) -> m a
  
-- MonadCatch instances should obey the following law:
catch (throwM e) f ≡ f e
  

You may forget catching exceptions, that finally crash your application.

{-# LANGUAGE PackageImports #-}

import "exceptions" Control.Monad.Catch (MonadThrow(..), MonadCatch(..), try)

data MyException = MyExcCase1 String | MyExcCase2 
  deriving (Eq, Show)

instance Exception MyException  -- makes an Exception instance

action1 :: MonadThrow m => m Int
action1 = do
            ... 
            when condition $ throwM $ MyExcCase1 "message"


-- try :: (MonadCatch m, Exception e) => m a -> m (Either e a) 

action2A :: (MonadCatch m) => m (Either MyException Int)
action2A = do
            ...
            try action1   -- catches exception that returns as an Either
            
-- forgetting to catch your exceptions
action2B :: (MonadCatch m) => m Int
acrion2B = do
             ...
             action1  -- it may crash if exception not catched in the code upwards
             ...

See also Catching all exceptions although it is about problems with asynchronous exceptions.

-- exception pkg replacement as recommended in "Catching all exceptions"

-- import "exceptions" Control.Monad.Catch

import "safe-exceptions" Control.Exception.Safe

Throwable errors that cannot escape the monad - ExceptT

ExceptT is a monad transformer that parameterises computations with the error type.

Because the main function must have type IO a, you will have to unwrap your ExceptT transformer (with runExceptT) at some point, and your thrown error will not escape the transformed monad!!, giving you an Either error result to analyze, unlike non-caught exceptions.

-- The ExceptT monad transformer

newtype ExceptT err m a = ExceptT (m (Either err a))
-- ^ err: the error type
-- ^ m: the inner monad

-- the ExceptT unwrapper
runExceptT :: ExceptT err m a -> m (Either err a)

throwE :: (Monad m) => err -> ExceptT err m a
throwE = ExceptT . return . Left

catchE :: (Monad m) =>
    ExceptT err m a               -- ^ the inner computation
    -> (err -> ExceptT err' m a)    -- ^ a handler for exceptions in the inner
                                -- computation
    -> ExceptT err' m a
    
catchE m handler = ExceptT $ do
    a <- runExceptT m
    case a of
        Left  err -> runExceptT (handler err)
        Right r -> return (Right r)

-- `withExceptT` evaluates a monadic action of a different error type, 
-- lifting the error to the present action error type, as shown below
withExceptT :: Functor m => (e -> e') -> ExceptT e m a -> ExceptT e' m a

The Except monad

(Except err) is a wrapper for failable functional computations in the Identity monad

-- Identity is the identity monad from "base" Data.Functor.Identity

type Except err :: * -> * = ExceptT e Identity

-- the wrapper
except :: Either err a -> Except err a
except = Identity >>> ExceptT

-- the unwrapper
runExcept :: Except err a -> Either err a
runExcept = runExceptT >>> runIdentity

Example of use: Simple nesting of error throwing actions. The error type of the outer action is a discriminated union (sum type) of own and child actions errors. I don't use the type alias Except to put emphasis on ExceptT

import "transformers" Control.Monad.Trans.Except
import Control.Monad (when)
import Data.Functor.Identity -- this would not be necessary using the type alias Except

data Err2 = Err2Cons1 String | Err2_Other deriving (Eq, Show)

data Err1 = Err1Cons1 String | Err1Cons2 Err2 deriving (Eq, Show)

-- throwing an error bypasses the rest of the computation

action3 :: ExceptT Err2 Identity Int
action3 = do
            x <- return someCalculation
            when (x `notElem` expectedValues) $ throwE $ Err2Cons1 "Unexpected!" 
            when condition2 $ throwE Err2_Other
            return x
            
action2 :: ExceptT Err2 Identity Int
action2 = catchE action3 $ \err -> case err of
                                Err2Cons1 msg -> return 0
                                Err2_Other -> throwE err -- rethrow

action1 :: ExceptT Err1 Identity Int
action1 = do
            when condition $ throwE $ Err1Cons1 "err1 msg"
            
            withExceptT Err1Cons2 action2   -- evaluates action2 lifting the Err2 type to an Err1
       
main :: IO ()
main = do
          let eRes = runExcept action1
          case eRes of
            Left err1 -> putStrLn $ "error: " ++ show err1
            Right v -> putStrLn $ "ok: " ++ show v

Leveraging exceptions into monad errors

{-# LANGUAGE PackageImports #-}

import Control.Monad (when)
import "transformers" Control.Monad.Trans.Except
import "exceptions" Control.Monad.Catch

data Err2 = Err2Cons1 String | Err2_Other deriving (Eq, Show)

instance Exception Err2

data Err1 = Err1Cons1 String | Err1Cons2 Err2 deriving (Eq, Show)

-- an exception throwing action
action2 :: IO Int
action2 = do
            x <- return someCalculation
            when (x `notElem` expectedValues) $ throwM $ Err2Cons1 "Unexpected!" 
            ...
            return x

-- an error throwing action
action1 :: ExceptT Err1 IO Int
action1 = do
            -- set the condition at will
            when condition $ throwE $ Err1Cons1 "err1 msg"

            -- wrapping an exception throwing action
            withExceptT Err1Cons2 $ ExceptT (try action2)
       
main :: IO ()
main = do
          eRes <- runExceptT action1
          case eRes of
            Left err1 -> putStrLn $ "error: " ++ show err1
            Right v -> putStrLn $ "ok: " ++ show v