{- | Module : Control.Monad.Script Description : An unrolled stack of Reader, Writer, Error, State, and Prompt transformers. Copyright : 2018, Automattic, Inc. License : BSD3 Maintainer : Nathan Bloomfield (nbloomf@gmail.com) Stability : experimental Portability : POSIX `ScriptT` is an unrolled stack of reader, writer, state, error, and prompt monad transformers, meant as a basis for building more specific DSLs. Also comes in "monad transformer transformer" flavor with `ScriptTT`. The addition of prompt to the monad team makes it straightforward to build effectful computations which defer the actual effects (and effect types) to an evaluator function that is both precisely controlled and easily extended. This allows us to build testable and composable API layers. The name "script" is meant to evoke the script of a play. In the theater sense a script is not a list of /instructions/ so much as a list of /suggestions/, and every cast gives a unique interpretation. Similarly a 'ScriptT eff a' is a pure value that gets an effectful interpretation in monad `eff` from a user-supplied evaluator. -} {-# LANGUAGE GADTs, Rank2Types, TupleSections, KindSignatures, ScopedTypeVariables, QuantifiedConstraints #-} module Control.Monad.Script ( -- * ScriptT ScriptT -- * ScriptTT , ScriptTT() , execScriptTT , liftScriptTT -- * Error , except , triage , throw , catch -- * Reader , ask , local , transport , reader -- * Writer , tell , draft , listen , pass , censor -- * State , get , put , modify , modify' , gets -- * Prompt , prompt -- * Testing , checkScriptTT ) where import Control.Monad ( ap, join ) import Control.Monad.Trans.Class ( MonadTrans(..) ) import Control.Monad.Trans.Identity ( IdentityT(..) ) import Data.Functor.Classes () import Data.Functor.Identity ( Identity(..) ) import Data.Monoid () import Data.Typeable ( Typeable ) import Test.QuickCheck ( Property, Gen, Arbitrary(..), CoArbitrary(..) ) import Test.QuickCheck.Monadic ( monadicIO, run, assert ) -- | Opaque stack of error (@e@), reader (@r@), writer (@w@), state (@s@), and prompt (@p@) monad transformers, accepting a monad transformer parameter (@t@). Behaves something like a monad transformer transformer. data ScriptTT (e :: *) (r :: *) (w :: *) (s :: *) (p :: * -> *) (t :: (* -> *) -> * -> *) (eff :: * -> *) (a :: *) where ScriptTT :: (Monad eff, Monad (t eff), MonadTrans t) => ((s,r) -> forall v. ((Either e a, s, w) -> t eff v) -> (forall u. p u -> (u -> t eff v) -> t eff v) -> t eff v) -> ScriptTT e r w s p t eff a deriving Typeable -- Only needed to make type inference work correctly. runScriptTT :: ScriptTT e r w s p t eff a -> (s,r) -> forall v. ((Either e a, s, w) -> t eff v) -> (forall u. p u -> (u -> t eff v) -> t eff v) -> t eff v runScriptTT (ScriptTT x) = x instance (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => Monad (ScriptTT e r w s p t eff) where return x = ScriptTT $ \(s,_) -> \end _ -> end (Right x, s, mempty) x >>= f = ScriptTT $ \(s0,r) -> \end cont -> do let g (z1,s1,w1) = case z1 of Right y -> do let h (z2,s2,w2) = end (z2, s2, mappend w1 w2) runScriptTT (f y) (s1,r) h cont Left e -> do let h (_,s2,w2) = end (Left e, s2, mappend w1 w2) runScriptTT (return ()) (s1,r) h cont runScriptTT x (s0,r) g cont instance (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => Applicative (ScriptTT e r w s p t eff) where pure = return (<*>) = ap instance (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => Functor (ScriptTT e r w s p t eff) where fmap f x = x >>= (return . f) instance (Monoid w, forall m. (Monad m) => Monad (t m), MonadTrans t) => MonadTrans (ScriptTT e r w s p t) where lift x = ScriptTT $ \(s,_) -> \end _ -> lift x >>= \a -> end (Right a, s, mempty) -- | Lift a value from the inner transformer. liftScriptTT :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => t eff a -> ScriptTT e r w s p t eff a liftScriptTT x = ScriptTT $ \(s,_) -> \end _ -> do a <- x end (Right a, s, mempty) -- | Opaque stack of error (@e@), reader (@r@), writer (@w@), state (@s@), and prompt (@p@) monad transformers. type ScriptT e r w s p = ScriptTT e r w s p IdentityT -- Execute a `ScriptTT` with a specified initial state, environment, and continuation. execScriptTC :: s -- ^ Initial state -> r -- ^ Environment -> ((Either e a, s, w) -> t eff v) -> (forall u. p u -> (u -> t eff v) -> t eff v) -> ScriptTT e r w s p t eff a -> t eff v execScriptTC s r end cont (ScriptTT run) = run (s,r) end cont -- | Execute a `ScriptTT` with a specified inital state and environment and with a specified prompt evaluator into the effect monad @eff@. execScriptTT :: (Monad eff, Monad (t eff), MonadTrans t) => s -- ^ Initial state -> r -- ^ Environment -> (forall u. p u -> eff u) -- ^ Monadic effect evaluator -> ScriptTT e r w s p t eff a -> t eff (Either e a, s, w) execScriptTT s r eval = execScriptTC s r return (\p c -> (lift $ eval p) >>= c) -- | Turn a `ScriptTT` with a monadic evaluator into a `Property`; for testing with QuickCheck. Wraps `execScriptTT`. checkScriptTT :: (Monad eff, Monad (t eff), MonadTrans t, Show q) => s -- ^ Initial state -> r -- ^ Environment -> (forall u. p u -> eff u) -- ^ Moandic effect evaluator -> (t eff (Either e a, s, w) -> IO q) -- ^ Condense to `IO` -> (q -> Bool) -- ^ Result check -> ScriptTT e r w s p t eff a -> Property checkScriptTT s r eval cond check script = monadicIO $ do let result = execScriptTT s r eval script q <- run $ cond result assert $ check q -- | Retrieve the environment. ask :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => ScriptTT e r w s p t eff r ask = ScriptTT $ \(s,r) -> \end _ -> end (Right r, s, mempty) -- | Run an action with a locally adjusted environment of the same type. local :: (Monad eff, Monad (t eff), MonadTrans t) => (r -> r) -> ScriptTT e r w s p t eff a -> ScriptTT e r w s p t eff a local = transport -- | Run an action with a locally adjusted environment of a possibly different type. transport :: (Monad eff, Monad (t eff), MonadTrans t) => (r2 -> r1) -> ScriptTT e r1 w s p t eff a -> ScriptTT e r2 w s p t eff a transport f x = ScriptTT $ \(s,r) -> \end cont -> runScriptTT x (s, f r) end cont -- | Retrieve the image of the environment under a given function. reader :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t, Monad (t eff)) => (r -> a) -> ScriptTT e r w s p t eff a reader f = fmap f ask -- | Retrieve the current state. get :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => ScriptTT e r w s p t eff s get = ScriptTT $ \(s,_) -> \end _ -> end (Right s, s, mempty) -- | Replace the state. put :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => s -> ScriptTT e r w s p t eff () put s = ScriptTT $ \(_,_) -> \end _ -> end (Right (), s, mempty) -- | Modify the current state lazily. modify :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => (s -> s) -> ScriptTT e r w s p t eff () modify f = ScriptTT $ \(s,_) -> \end _ -> end (Right (), f s, mempty) -- | Modify the current state strictly. modify' :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => (s -> s) -> ScriptTT e r w s p t eff () modify' f = ScriptTT $ \(s,_) -> \end _ -> end (Right (), f $! s, mempty) -- | Retrieve the image of the current state under a given function. gets :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => (s -> a) -> ScriptTT e r w s p t eff a gets f = ScriptTT $ \(s,_) -> \end _ -> end (Right (f s), s, mempty) -- | Write to the log. tell :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => w -> ScriptTT e r w s p t eff () tell w = ScriptTT $ \(s,_) -> \end _ -> end (Right (), s, w) -- | Run an action and attach the log to the result, setting the log to `mempty`. draft :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => ScriptTT e r w s p t eff a -> ScriptTT e r w s p t eff (a,w) draft x = ScriptTT $ \(r,s) -> \end cont -> runScriptTT x (r,s) (\(y,s,w) -> end (fmap (,w) y, s, mempty)) cont -- | Run an action and attach the log to the result. listen :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => ScriptTT e r w s p t eff a -> ScriptTT e r w s p t eff (a,w) listen x = ScriptTT $ \(r,s) -> \end cont -> runScriptTT x (r,s) (\(y,s,w) -> end (fmap (,w) y, s, w)) cont -- | Run an action that returns a value and a log-adjusting function, and apply the function to the local log. pass :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => ScriptTT e r w s p t eff (a, w -> w) -> ScriptTT e r w s p t eff a pass x = ScriptTT $ \(r,s) -> \end cont -> let end' (z,s1,w) = case z of Right (y,f) -> end (Right y, s1, f w) Left e -> end (Left e, s1, w) in runScriptTT x (r,s) end' cont -- | Run an action, applying a function to the local log. censor :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => (w -> w) -> ScriptTT e r w s p t eff a -> ScriptTT e r w s p t eff a censor f x = pass $ ScriptTT $ \(s,r) -> \end cont -> let end' (z,s1,w) = case z of Right y -> end (Right (y,f), s1, w) Left e -> end (Left e, s1, w) in runScriptTT x (s,r) end' cont -- | Inject an 'Either' into a 'Script'. except :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => Either e a -> ScriptTT e r w s p t eff a except z = ScriptTT $ \(s,_) -> \end _ -> end (z, s, mempty) -- | Run an action, applying a function to any error. triage :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => (e1 -> e2) -> ScriptTT e1 r w s p t eff a -> ScriptTT e2 r w s p t eff a triage f x = ScriptTT $ \(s,r) -> \end cont -> let end' (z,s1,w) = case z of Right y -> end (Right y, s1, w) Left e -> end (Left (f e), s1, w) in runScriptTT x (s,r) end' cont -- | Raise an error. throw :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => e -> ScriptTT e r w s p t eff a throw e = ScriptTT $ \(s,r) -> \end cont -> let end' (_,s1,w1) = end (Left e, s1, w1) in runScriptTT (return ()) (s,r) end' cont -- | Run an action, applying a handler in case of an error result. catch :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => ScriptTT e r w s p t eff a -> (e -> ScriptTT e r w s p t eff a) -> ScriptTT e r w s p t eff a catch (ScriptTT x) h = ScriptTT $ \(s,r) -> \end cont -> let end' (z,s1,w) = case z of Right y -> end (Right y, s1, w) Left e -> do let end'' (z2,s2,w2) = end (z2, s2, mappend w w2) runScriptTT (h e) (s1,r) end'' cont in x (s,r) end' cont -- | Inject an atomic effect. prompt :: (Monoid w, Monad eff, Monad (t eff), MonadTrans t) => p a -> ScriptTT e r w s p t eff a prompt p = ScriptTT $ \(s,_) -> \end cont -> cont p (\a -> end (Right a, s, mempty)) instance ( Monoid w, Monad eff, forall m. Monad m => Monad (t m), MonadTrans t , Arbitrary a, CoArbitrary a ) => Arbitrary (ScriptTT e r w s p t eff a) where arbitrary = do (a,b) <- arbitrary :: Gen (a,a) k <- arbitrary :: Gen Int if k`rem`2 == 0 then return $ return a else do f <- arbitrary :: Gen (a -> ScriptTT e r w s p t eff a) return $ f a >> lift (return b) instance Show (ScriptTT e r w s p t eff a) where show _ = "