{- |
Module      :  Control.Monad.Trans.Peel
Copyright   :  © Anders Kaseorg, 2010
License     :  BSD-style

Maintainer  :  Anders Kaseorg <andersk@mit.edu>
Stability   :  experimental
Portability :  portable

This module defines the class 'MonadTransPeel' of monad transformers
through which control operations can be lifted.  Instances are
included for all the standard monad transformers from the
@transformers@ library except @ContT@.

'idPeel' and 'liftPeel' are provided to assist creation of
@MonadPeelIO@-like classes (see "Control.Monad.IO.Peel") based on core
monads other than 'IO'.
-}

module Control.Monad.Trans.Peel (
  MonadTransPeel(..),
  idPeel,
  liftPeel,
  ) where

import Prelude hiding (catch)
import Control.Monad
import Control.Monad.Trans.Class
import Control.Monad.Trans.Identity
import Control.Monad.Trans.List
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Error
import Control.Monad.Trans.Reader
import Control.Monad.Trans.State
import qualified Control.Monad.Trans.State.Strict as Strict
import Control.Monad.Trans.Writer
import qualified Control.Monad.Trans.Writer.Strict as Strict
import qualified Control.Monad.Trans.RWS as RWS
import qualified Control.Monad.Trans.RWS.Strict as RWS.Strict
import Data.Monoid


-- |@MonadTransPeel@ is the class of monad transformers supporting an
-- extra operation 'peel', enabling control operations (functions that
-- use monadic actions as input instead of just output) to be lifted
-- through the transformer.
class MonadTrans t => MonadTransPeel t where
  -- |@peel@ is used to peel off the outer layer of a transformed
  -- monadic action, allowing an transformed action @t m a@ to be
  -- treated as a base action @m b@.
  --
  -- More precisely, @peel@ captures the monadic state of @t@ at the
  -- point where it is bound (in @t n@), yielding a function @t m a ->
  -- m (t o a)@; this function runs a transformed monadic action @t m
  -- a@ in the base monad @m@ using the captured state, and leaves the
  -- result @t o a@ in the monad @m@ after all side effects in @m@
  -- have occurred.
  --
  -- This can be used together with 'lift' to lift control operations
  -- with types such as @M a -> M a@ into the transformed monad @t M@:
  --
  -- @
  --    instance Monad M
  --    foo :: M a -> M a
  --    foo' :: ('MonadTransPeel' t, 'Monad' (t M)) => t M a -> t M a
  --    foo' a = do
  --      k \<- 'peel'  -- k :: t M a -> M (t M a)
  --      'join' $ 'lift' $ foo (k a)  -- uses foo :: M (t M a) -> M (t M a)
  -- @
  --
  -- @peel@ is typically used with @m == n == o@, but is required to
  -- be polymorphic for greater type safety: for example, this type
  -- ensures that the result of running the action in @m@ has no
  -- remaining side effects in @m@.
  peel :: (Monad m, Monad n, Monad o) => t n (t m a -> m (t o a))

instance MonadTransPeel IdentityT where
  peel = return $ \m -> do
    x <- runIdentityT m
    return $ return x

liftList :: Monad m => [a] -> ListT m a
liftList = ListT . return

instance MonadTransPeel ListT where
  peel = return $ \m -> do
    xs <- runListT m
    return $ liftList xs

instance MonadTransPeel MaybeT where
  peel = return $ \m -> do
    xm <- runMaybeT m
    return $ maybe mzero return xm

instance Error e => MonadTransPeel (ErrorT e) where
  peel = return $ \m -> do
    xe <- runErrorT m
    return $ either throwError return xe

instance MonadTransPeel (ReaderT r) where
  peel = asks $ \r m -> do
    x <- runReaderT m r
    return $ return x

instance MonadTransPeel (StateT s) where
  peel = gets $ \s m -> do
    (x, s') <- runStateT m s
    return $ do
      put s'
      return x
instance MonadTransPeel (Strict.StateT s) where
  peel = Strict.gets $ \s m -> do
    (x, s') <- Strict.runStateT m s
    return $ do
      Strict.put s'
      return x

instance Monoid w => MonadTransPeel (WriterT w) where
  peel = return $ \m -> do
    (x, w) <- runWriterT m
    return $ do
      tell w
      return x
instance Monoid w => MonadTransPeel (Strict.WriterT w) where
  peel = return $ \m -> do
    (x, w) <- Strict.runWriterT m
    return $ do
      Strict.tell w
      return x

instance Monoid w => MonadTransPeel (RWS.RWST r w s) where
  peel = do
    r <- RWS.ask
    s <- RWS.get
    return $ \m -> do
      (x, s', w) <- RWS.runRWST m r s
      return $ do
        RWS.put s'
        RWS.tell w
        return x
instance Monoid w => MonadTransPeel (RWS.Strict.RWST r w s) where
  peel = do
    r <- RWS.Strict.ask
    s <- RWS.Strict.get
    return $ \m -> do
      (x, s', w) <- RWS.Strict.runRWST m r s
      return $ do
        RWS.Strict.put s'
        RWS.Strict.tell w
        return x


-- |@idPeel@ acts as the \"identity\" 'peel' operation from a monad
-- @m@ to itself.
--
-- @
--    'idPeel' = 'return' $ 'liftM' 'return'
-- @
--
-- It serves as the base case for a class like @MonadPeelIO@, which
-- allows control operations in some base monad (here @IO@) to be
-- lifted through arbitrary stacks of zero or more monad transformers
-- in one call.  For example, "Control.Monad.IO.Peel" defines
--
-- @
--    class 'MonadIO' m => MonadPeelIO m where
--      peelIO :: m (m a -> 'IO' (m a))
--    instance MonadPeelIO 'IO' where
--      peelIO = 'idPeel'
-- @
idPeel :: (Monad m, Monad n, Monad o) => n (m a -> m (o a))
idPeel = return $ liftM return

-- |@liftPeel@ is used to compose two 'peel' operations: the outer
-- provided by a 'MonadTransPeel' instance, and the inner provided as
-- the argument.
--
-- It satisfies @'liftPeel' 'idPeel' == 'peel'@.
--
-- It serves as the induction step of a @MonadPeelIO@-like class.  For
-- example, "Control.Monad.IO.Peel" defines
--
-- @
--    instance MonadPeelIO m => MonadPeelIO ('StateT' s m) where
--      peelIO = 'liftPeel' peelIO
-- @
--
-- using the 'MonadTransPeel' instance of @'StateT' s@.
liftPeel :: (MonadTransPeel t, Monad m, Monad m', Monad n', Monad (t n'),
             Monad o', Monad (t o')) =>
            n' (m' (t o' a) -> m (o' (t o' a))) -> t n' (t m' a -> m (t o' a))
liftPeel p = do
  k <- peel
  lift $ do
    k' <- p
    return $ \m -> do
      m' <- k' $ k m
      return $ join $ lift m'