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