monad-chronicle-1: src/Control/Monad/Trans/Chronicle.hs
{-# LANGUAGE Trustworthy #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
-----------------------------------------------------------------------------
-- | Module : Control.Monad.Chronicle
--
-- Hybrid error/writer monad class that allows both accumulating outputs and
-- aborting computation with a final output.
--
-- The expected use case is for computations with a notion of fatal vs.
-- non-fatal errors.
-----------------------------------------------------------------------------
module Control.Monad.Trans.Chronicle (
-- * The Chronicle monad
Chronicle, chronicle, runChronicle,
-- * The ChronicleT monad transformer
ChronicleT(..),
-- * Chronicle operations
dictate, disclose, confess,
memento, absolve, condemn,
retcon,
) where
import Control.Applicative
import Control.Monad
import Control.Monad.Fix
import Control.Monad.Trans
import Data.Default.Class
import Data.Functor.Identity
import Data.Semigroup
import Control.Monad.Error.Class
import Control.Monad.Reader.Class
import Control.Monad.RWS.Class
import Prelude
import Data.These
import Data.These.Combinators (mapHere)
#ifdef MIN_VERSION_semigroupoids
import Data.Functor.Apply (Apply(..))
import Data.Functor.Bind (Bind(..))
#endif
-- --------------------------------------------------------------------------
-- | A chronicle monad parameterized by the output type @c@.
--
-- The 'return' function produces a computation with no output, and '>>='
-- combines multiple outputs with '<>'.
type Chronicle c = ChronicleT c Identity
chronicle :: Monad m => These c a -> ChronicleT c m a
chronicle = ChronicleT . return
runChronicle :: Chronicle c a -> These c a
runChronicle = runIdentity . runChronicleT
-- --------------------------------------------------------------------------
-- | The `ChronicleT` monad transformer.
--
-- The 'return' function produces a computation with no output, and '>>='
-- combines multiple outputs with '<>'.
newtype ChronicleT c m a = ChronicleT { runChronicleT :: m (These c a) }
instance (Functor m) => Functor (ChronicleT c m) where
fmap f (ChronicleT c) = ChronicleT (fmap f <$> c)
#ifdef MIN_VERSION_semigroupoids
instance (Semigroup c, Apply m) => Apply (ChronicleT c m) where
ChronicleT f <.> ChronicleT x = ChronicleT ((<.>) <$> f <.> x)
instance (Semigroup c, Apply m, Monad m) => Bind (ChronicleT c m) where
(>>-) = (>>=)
#endif
instance (Semigroup c, Applicative m) => Applicative (ChronicleT c m) where
pure = ChronicleT . pure . pure
ChronicleT f <*> ChronicleT x = ChronicleT (liftA2 (<*>) f x)
instance (Semigroup c, Monad m) => Monad (ChronicleT c m) where
return = ChronicleT . return . return
m >>= k = ChronicleT $
do cx <- runChronicleT m
case cx of
This a -> return (This a)
That x -> runChronicleT (k x)
These a x -> do cy <- runChronicleT (k x)
return $ case cy of
This b -> This (a <> b)
That y -> These a y
These b y -> These (a <> b) y
instance (Semigroup c) => MonadTrans (ChronicleT c) where
lift m = ChronicleT (That `liftM` m)
instance (Semigroup c, MonadIO m) => MonadIO (ChronicleT c m) where
liftIO = lift . liftIO
instance (Semigroup c, Monoid c, Applicative m, Monad m) => Alternative (ChronicleT c m) where
empty = mzero
(<|>) = mplus
instance (Semigroup c, Monoid c, Monad m) => MonadPlus (ChronicleT c m) where
mzero = confess mempty
mplus x y = do x' <- memento x
case x' of
Left _ -> y
Right r -> return r
instance (Semigroup c, MonadError e m) => MonadError e (ChronicleT c m) where
throwError = lift . throwError
catchError (ChronicleT m) c = ChronicleT $ catchError m (runChronicleT . c)
instance (Semigroup c, MonadReader r m) => MonadReader r (ChronicleT c m) where
ask = lift ask
local f (ChronicleT m) = ChronicleT $ local f m
reader = lift . reader
instance (Semigroup c, MonadRWS r w s m) => MonadRWS r w s (ChronicleT c m) where
instance (Semigroup c, MonadState s m) => MonadState s (ChronicleT c m) where
get = lift get
put = lift . put
state = lift . state
instance (Semigroup c, MonadWriter w m) => MonadWriter w (ChronicleT c m) where
tell = lift . tell
listen (ChronicleT m) = ChronicleT $ do
(m', w) <- listen m
return $ case m' of
This c -> This c
That x -> That (x, w)
These c x -> These c (x, w)
pass (ChronicleT m) = ChronicleT $ do
pass $ these (\c -> (This c, id))
(\(x, f) -> (That x, f))
(\c (x, f) -> (These c x, f)) `liftM` m
writer = lift . writer
-- this is basically copied from the instance for Either in transformers
-- need to test this to make sure it's actually sensible...?
instance (Semigroup c, MonadFix m) => MonadFix (ChronicleT c m) where
mfix f = ChronicleT (mfix (runChronicleT . f . these (const bomb) id (flip const)))
where bomb = error "mfix (ChronicleT): inner compuation returned This value"
-- | @'dictate' c@ is an action that records the output @c@.
--
-- Equivalent to 'tell' for the 'Writer' monad.
dictate :: (Semigroup c, Monad m) => c -> ChronicleT c m ()
dictate c = ChronicleT $ return (These c ())
-- | @'disclose' c@ is an action that records the output @c@ and returns a
-- @'Default'@ value.
--
-- This is a convenience function for reporting non-fatal errors in one
-- branch a @case@, or similar scenarios when there is no meaningful
-- result but a placeholder of sorts is needed in order to continue.
disclose :: (Default a, Semigroup c, Monad m) => c -> ChronicleT c m a
disclose c = dictate c >> return def
-- | @'confess' c@ is an action that ends with a final output @c@.
--
-- Equivalent to 'throwError' for the 'Error' monad.
confess :: (Semigroup c, Monad m) => c -> ChronicleT c m a
confess c = ChronicleT $ return (This c)
-- | @'memento' m@ is an action that executes the action @m@, returning either
-- its record if it ended with 'confess', or its final value otherwise, with
-- any record added to the current record.
--
-- Similar to 'catchError' in the 'Error' monad, but with a notion of
-- non-fatal errors (which are accumulated) vs. fatal errors (which are caught
-- without accumulating).
memento :: (Semigroup c, Monad m) => ChronicleT c m a -> ChronicleT c m (Either c a)
memento m = ChronicleT $
do cx <- runChronicleT m
return $ case cx of
This a -> That (Left a)
That x -> That (Right x)
These a x -> These a (Right x)
-- | @'absolve' x m@ is an action that executes the action @m@ and discards any
-- record it had. The default value @x@ will be used if @m@ ended via
-- 'confess'.
absolve :: (Semigroup c, Monad m) => a -> ChronicleT c m a -> ChronicleT c m a
absolve x m = ChronicleT $
do cy <- runChronicleT m
return $ case cy of
This _ -> That x
That y -> That y
These _ y -> That y
-- | @'condemn' m@ is an action that executes the action @m@ and keeps its value
-- only if it had no record. Otherwise, the value (if any) will be discarded
-- and only the record kept.
--
-- This can be seen as converting non-fatal errors into fatal ones.
condemn :: (Semigroup c, Monad m) => ChronicleT c m a -> ChronicleT c m a
condemn (ChronicleT m) = ChronicleT $ do
m' <- m
return $ case m' of
This x -> This x
That y -> That y
These x _ -> This x
-- | @'retcon' f m@ is an action that executes the action @m@ and applies the
-- function @f@ to its output, leaving the return value unchanged.
--
-- Equivalent to 'censor' for the 'Writer' monad.
retcon :: (Semigroup c, Monad m) => (c -> c) -> ChronicleT c m a -> ChronicleT c m a
retcon f m = ChronicleT $ mapHere f `liftM` runChronicleT m