{-# LANGUAGE CPP #-} -- | = Introduction -- -- This module gives an alternative to the Operational package \[1\], in which -- instructions can be higher-order functors, parameterized on the program monad -- that they are part of. This makes it possible to define instruction sets -- compositionally using ':+:'. In the normal Operational, this could be done -- for simple instructions, but here it can be done even for \"control -- instructions\" -- instructions that take programs as arguments. -- -- For general information about the ideas behind this module, see the -- Operational package \[1\]. -- -- \[1\] <http://hackage.haskell.org/package/operational> -- -- = Example -- -- (Full code found in -- <https://github.com/emilaxelsson/operational-alacarte/blob/master/examples/Simple.hs>.) -- -- An \"if\" instruction can be defined as follows: -- -- @ -- data If fs a where -- If :: Exp `Bool` -> prog a -> prog a -> If (`Param1` prog) a -- @ -- -- Note the use of the type parameter @prog@ to refer to sub-programs. (@Exp@ is -- some type representing pure expressions.) -- -- The type @(`Param1` prog)@ can be seen as a type-level list with one element -- @prog@. (See "Data.ALaCarte" for more details on parameter lists.) -- -- We can now make program types that combine several instructions; e.g.: -- -- @type MyProgram = `Program` (If `:+:` Loop `:+:` ...) `Param0`@ -- -- 'Program' is a recursive type that sets the type of the sub-programs of @If@ -- (and @Loop@, etc.) to @MyProgram@. With the original Operational package, we -- would have to hard-code a specific type for the sub-programs of @If@ (or make -- @MyProgram@ a recursive newtype, as noted by the author of Operational). -- -- Interpretation of 'Program' in a monad @m@ can be done using -- -- @`interpret` :: (`Interp` i m fs, `HFunctor` i, `Monad` m) => `Program` i fs a -> m a@ -- -- In order to use this function, @If@ needs to be an instance of 'Interp' and -- 'HFunctor'. The 'HFunctor' instance is straightforward: -- -- @ -- instance `HFunctor` If where -- `hfmap` f (If c thn els) = If c (f thn) (f els) -- @ -- -- The 'Interp' type class is parameterized both on the instruction type and the -- destination monad. For example, interpretation of @If@ in the 'IO' monad -- might look as follows: -- -- @ -- instance `Interp` If `IO` fs where -- `interp` (If c thn els) = if eval c then thn else els -- @ -- -- (Here @eval@ is the evaluator for the expression languauge @Exp@.) -- -- The 'Interp' class distributes over ':+:' which means that it is possible to -- interpret any expression type @(I1 `:+:` I2 `:+:` I3 `:+:` ...)@ to 'IO', as -- long as the individual instructions (@I1@, @I2@, etc.) have 'Interp' -- instances for 'IO'. -- -- = Bi-functorial instructions -- -- The type @(`Param1` prog)@ in the result of @If@ above says that the -- instruction has one sub-structure whose type is @prog@. The advanced example -- <https://github.com/emilaxelsson/operational-alacarte/blob/master/examples/Advanced.hs> -- shows a version of @If@ that has two parameters: -- -- @ -- If :: exp `Bool` -> prog a -> prog a -> If (`Param2` prog exp) a -- @ -- -- @prog@ represents sub-programs and @exp@ represents expressions (@Exp Bool@ -- has been replaced with @exp Bool@). -- -- This new @If@ instruction is a higher-order /bi-functor/ (see 'HBifunctor'). -- -- The advantage of parameterizing instructions on expressions is that it lets -- us define generic functions, such as `interpretBi`, which decouple the -- interpretation of instructions from the interpretation of expressions. -- -- = Generalized interface -- -- We have seen two examples of @If@ with a parameter list of one and two -- arguments respectively (@(`Param1` prog)@ and @(`Param2` prog exp)@). There -- is nothing stopping us from having instructions with more parameters. For -- example, we could make a version of @If@ that takes an extra type class -- constraint @pred@ as parameter: -- -- @ -- If :: pred a => exp `Bool` -> prog a -> prog a -> If (`Param3` prog exp pred) a -- @ -- -- (See the documentation to "Data.ALaCarte" regarding why it is a good idea to -- make @pred@ part of the parameter list rather than just making it a separate -- parameter.) -- -- In fact, it is possible to have arbitrarily many parameters to instructions -- (but the type synonyms for parameter lists stop at 'Param4'). -- -- The functions and types in this module (and "Data.ALaCarte") are designed to -- be generic in the sense that things that work for parameter lists of /N/ -- elements also work for parameter lists of more elements. For example, the -- function 'interpret' mentioned above requires the instruction @i@ to be a -- higher-order functor, but it also works for high-order bi-functors, and for -- the last version of @If@ that has an additional parameter @pred@. -- -- = Typical use -- -- Here we give specialized type signatures for a selection of functions for -- different uses of the general interface. -- -- . -- -- __Functorial instructions with no extra parameters__ -- -- This scenario is used in <https://github.com/emilaxelsson/operational-alacarte/blob/master/examples/Simple.hs>. -- -- @ -- `singleton` :: instr (`Param1` (`ProgramT` instr `Param0` m)) a -> `ProgramT` instr `Param0` m a -- -- `interpretWithMonad` -- :: (`HFunctor` instr, `Monad` m) -- => (forall b . instr (`Param1` m) b -> m b) -- -> `Program` instr `Param0` a -> m a -- -- `interpret` -- :: (`Interp` i m `Param0`, `HFunctor` i, `Monad` m) -- => `Program` i `Param0` a -> m a -- @ -- -- . -- -- __Functorial instructions with extra parameter__ -- -- Like the previous scenario but with an extra parameter @p@ (poly-kinded) that instructions can use for anything. -- -- @ -- `singleton` :: instr (`Param2` (`ProgramT` instr (`Param1` p) m) p) a -> `ProgramT` instr (`Param1` p) m a -- -- `interpretWithMonad` -- :: (`HFunctor` instr, `Monad` m) -- => (forall b . instr (`Param2` m p) b -> m b) -- -> `Program` instr (`Param1` p) a -> m a -- -- `interpret` -- :: (`Interp` i m (`Param1` p), `HFunctor` i, `Monad` m) -- => `Program` i (`Param1` p) a -> m a -- @ -- -- . -- -- __Bi-functorial instructions with no extra parameters__ -- -- This scenario is used in <https://github.com/emilaxelsson/operational-alacarte/blob/master/examples/Advanced.hs>. -- -- The parameter @exp@ is an extra interpreted structure that e.g. can represent expressions. -- -- @ -- `singleton` :: instr (`Param2` (`ProgramT` instr (`Param1` exp) m) exp) a -> `ProgramT` instr (`Param1` exp) m a -- -- `interpretWithMonadBi` -- :: (`HBifunctor` instr, `Functor` m, `Monad` m) -- => (forall b . exp b -> m b) -- -> (forall b . instr (`Param2` m m) b -> m b) -- -> `Program` instr (`Param1` exp) a -> m a -- -- `interpret` -- :: (`InterpBi` i m `Param0`, `HBifunctor` i, `Functor` m, `Monad` m) -- => (forall b . exp b -> m b) -> `Program` i (`Param1` exp) a -> m a -- @ -- -- . -- -- __Bi-functorial instructions with extra parameter__ -- -- Like the previous scenario but with an extra parameter @p@ (poly-kinded) that instructions can use for anything. -- -- @ -- `singleton` :: instr (`Param3` (`ProgramT` instr (`Param2` exp p) m) exp p) a -> `ProgramT` instr (`Param2` exp p) m a -- -- `interpretWithMonadBi` -- :: (`HBifunctor` instr, `Functor` m, `Monad` m) -- => (forall b . exp b -> m b) -- -> (forall b . instr (`Param3` m m p) b -> m b) -- -> `Program` instr (`Param2` exp p) a -> m a -- -- `interpret` -- :: (`InterpBi` i m (`Param1` p), `HBifunctor` i, `Functor` m, `Monad` m) -- => (forall b . exp b -> m b) -> `Program` i (`Param2` exp p) a -> m a -- @ module Control.Monad.Operational.Higher ( module Control.Monad , module Data.ALaCarte -- * Program monad , ProgramT , Program , singleton , singleInj -- * Interpretation , liftProgram , interpretWithMonadT , interpretWithMonad , Interp (..) , interpretT , interpret , ProgramViewT (..) , ProgramView (..) , ProgViewT , ProgView , viewT , view , unview -- * Bi-functorial instructions , interpretWithMonadBiT , interpretWithMonadBi , InterpBi (..) , interpretBiT , interpretBi , Reexpressible (..) , reexpress , reexpressEnv ) where #if __GLASGOW_HASKELL__ < 710 import Control.Applicative #endif import Control.Monad import Control.Monad.Identity import Control.Monad.Reader import Data.Typeable import Data.ALaCarte -------------------------------------------------------------------------------- -- * Program monad -------------------------------------------------------------------------------- -- | Representation of programs parameterized by the primitive instructions -- (transformer version) data ProgramT instr fs m a where Lift :: m a -> ProgramT instr fs m a Bind :: ProgramT instr fs m a -> (a -> ProgramT instr fs m b) -> ProgramT instr fs m b Instr :: instr '(ProgramT instr fs m, fs) a -> ProgramT instr fs m a #if __GLASGOW_HASKELL__>=708 deriving Typeable #endif -- | Representation of programs parameterized by the primitive instructions type Program instr fs = ProgramT instr fs Identity instance Monad m => Functor (ProgramT instr fs m) where fmap = liftM instance Monad m => Applicative (ProgramT instr fs m) where pure = return (<*>) = ap instance Monad m => Monad (ProgramT instr fs m) where return = Lift . return (>>=) = Bind instance MonadTrans (ProgramT instr fs) where lift = Lift -- | Make a program from a single instruction singleton :: instr '(ProgramT instr fs m, fs) a -> ProgramT instr fs m a singleton = Instr -- | Make a program from a single instruction singleInj :: (i :<: instr) => i '(ProgramT instr fs m, fs) a -> ProgramT instr fs m a singleInj = Instr . inj -------------------------------------------------------------------------------- -- * Interpretation -------------------------------------------------------------------------------- -- | Lift a program to a program transformer liftProgram :: forall instr fs m a . (HFunctor instr, Monad m) => Program instr fs a -- ^ Program to lift -> ProgramT instr fs m a liftProgram = go where go :: Program instr fs b -> ProgramT instr fs m b go (Lift a) = Lift $ return $ runIdentity a go (Bind p k) = Bind (go p) (go . k) go (Instr i) = Instr $ hfmap go i -- | Interpret a program in a monad interpretWithMonadT :: forall instr fs m n a . (HFunctor instr, Monad m) => (forall b . instr '(m,fs) b -> m b) -- ^ Interpretation of instructions -> (forall b . n b -> m b) -- ^ Interpretation of the underlying monad -> ProgramT instr fs n a -> m a interpretWithMonadT inti intn = go where go :: ProgramT instr fs n b -> m b go (Lift a) = intn a go (Bind p k) = go p >>= (go . k) go (Instr i) = inti $ hfmap go i -- | Interpret a program in a monad interpretWithMonad :: (HFunctor instr, Monad m) => (forall b . instr '(m,fs) b -> m b) -- ^ Interpretation of instructions -> Program instr fs a -> m a interpretWithMonad interp = interpretWithMonadT interp (return . runIdentity) -- | @`Interp` instr m fs@ represents the fact that @instr@ can be interpreted -- in the monad @m@ class Interp instr m fs where -- | Interpret an instruction in a monad interp :: instr '(m,fs) a -> m a instance (Interp i1 m fs, Interp i2 m fs) => Interp (i1 :+: i2) m fs where interp (Inl i) = interp i interp (Inr i) = interp i -- | Interpret a program in a monad. The interpretation of instructions is -- provided by the 'Interp' class. interpretT :: (Interp i m fs, HFunctor i, Monad m) => (forall b . n b -> m b) -- ^ Interpretation of the underlying monad -> ProgramT i fs n a -> m a interpretT = interpretWithMonadT interp -- | Interpret a program in a monad. The interpretation of instructions is -- provided by the 'Interp' class. interpret :: (Interp i m fs, HFunctor i, Monad m) => Program i fs a -> m a interpret = interpretWithMonad interp -- | View type for inspecting the first instruction data ProgramViewT instr fs m a where Return :: a -> ProgramViewT instr fs m a (:>>=) :: instr '(ProgramT instr fs m, fs) b -> (b -> ProgramT instr fs m a) -> ProgramViewT instr fs m a -- | View type for inspecting the first instruction type ProgramView instr fs = ProgramViewT instr fs Identity type ProgViewT instr m = ProgramViewT instr '() m type ProgView instr = ProgramView instr '() -- | View function for inspecting the first instruction viewT :: Monad m => ProgramT instr fs m a -> m (ProgramViewT instr fs m a) viewT (Lift m) = m >>= return . Return viewT (Lift m `Bind` g) = m >>= viewT . g viewT ((m `Bind` g) `Bind` h) = viewT (m `Bind` (\x -> g x `Bind` h)) viewT (Instr i `Bind` g) = return (i :>>= g) viewT (Instr i) = return (i :>>= return) -- | View function for inspecting the first instruction view :: HFunctor instr => Program instr fs a -> ProgramView instr fs a view = runIdentity . viewT -- | Turn a 'ProgramViewT' back to a 'Program' unview :: Monad m => ProgramViewT instr fs m a -> ProgramT instr fs m a unview (Return a) = return a unview (i :>>= k) = singleton i >>= k -------------------------------------------------------------------------------- -- * Bi-functorial instructions -------------------------------------------------------------------------------- -- | Bi-functorial version of 'interpretWithMonadT' -- -- Bi-functorial instructions are of the form @instr '(prog, '(exp, ...)) a@, -- where @prog@ is a representation of sub-programs, and @exp@ is a -- representation of some other sub-structure, e.g. expressions. -- 'interpretWithMonadBiT' allows interpreting both these kinds of -- sub-structures in a generic way. interpretWithMonadBiT :: (HBifunctor instr, Functor m, Monad m, Monad n) => (forall b . exp b -> m b) -- ^ Interpretation of the @exp@ sub-structure -> (forall b . instr '(m,'(m,fs)) b -> m b) -- ^ Interpretation of instructions -> (forall b . n b -> m b) -- ^ Interpretation of the underlying monad -> ProgramT instr '(exp,fs) n a -> m a interpretWithMonadBiT inte inti intn = interpretWithMonadT (\i -> inti $ hbimap id inte i) intn -- | Bi-functorial version of 'interpretWithMonad' -- -- See explanation of 'interpretWithMonadBiT'. interpretWithMonadBi :: (HBifunctor instr, Functor m, Monad m) => (forall b . exp b -> m b) -- ^ Interpretation of the @exp@ sub-structure -> (forall b . instr '(m,'(m,fs)) b -> m b) -- ^ Interpretation of instructions -> Program instr '(exp,fs) a -> m a interpretWithMonadBi inte inti = interpretWithMonadBiT inte inti (return . runIdentity) -- | @`InterpBi` instr m fs@ represents the fact that the bi-functorial -- instruction @instr@ can be interpreted in the monad @m@ class InterpBi instr m fs where -- | Interpret a bi-functorial instruction in a monad interpBi :: instr '(m,'(m,fs)) a -> m a instance (InterpBi i1 m fs, InterpBi i2 m fs) => InterpBi (i1 :+: i2) m fs where interpBi (Inl i) = interpBi i interpBi (Inr i) = interpBi i -- | Interpret a program in a monad. The interpretation of instructions is -- provided by the 'InterpBi' class. interpretBiT :: (InterpBi i m fs, HBifunctor i, Functor m, Monad m, Monad n) => (forall b . exp b -> m b) -- ^ Interpretation of the @exp@ sub-structure -> (forall b . n b -> m b) -- ^ Interpretation of the underlying monad -> ProgramT i '(exp,fs) n a -> m a interpretBiT inte = interpretWithMonadBiT inte interpBi -- The reason for only getting the interpretation of instructions from a class -- (and not the interpretation of `exp`), is that `interpBi` is constructed -- automatically for instructions built using `:+:`. It would be quite -- cumbersome to construct this function by hand. -- | Interpret a program in a monad. The interpretation of instructions is -- provided by the 'InterpBi' class. interpretBi :: (InterpBi i m fs, HBifunctor i, Functor m, Monad m) => (forall b . exp b -> m b) -- ^ Interpretation of the @exp@ sub-structure -> Program i '(exp,fs) a -> m a interpretBi inte = interpretWithMonadBi inte interpBi -- | Reexpressible types -- -- @i@ is a bi-functorial instruction where, in the type @i '(p,'(e1,...)) a@, -- sub-structure @e1@ can be converted to a different sub-structure @e2@. -- -- @e1@ and @e2@ typically represent expressions; hence the name -- \"reexpressible\". class HBifunctor i => Reexpressible i instr env where -- | Rewrite an instruction changing its \"expression\" sub-structure -- -- As an example of how to define this function, take the following -- instruction that just puts a tag on a sub-program: -- -- @ -- data Tag fs a -- where -- Tag :: `String` -> prog () -> Tag (`Param2` prog exp) () -- @ -- -- To define `reexpressInstrEnv` we have to use a combination of `ReaderT` -- and `runReaderT`: -- -- @ -- instance (Tag `:<:` instr) => `Reexpressible` Tag instr -- where -- `reexpressInstrEnv` reexp (Tag tag prog) = `ReaderT` `$` \env -> -- `singleInj` `$` Tag tag (`flip` `runReaderT` env prog) -- @ reexpressInstrEnv :: Monad m => ( forall b . exp1 b -> ReaderT env (ProgramT instr '(exp2,fs) m) (exp2 b) ) -- ^ Conversion of the \"expression\" sub-structure -> i '(ReaderT env (ProgramT instr '(exp2,fs) m), '(exp1, fs)) a -> ReaderT env (ProgramT instr '(exp2,fs) m) a -- The reason for only allowing `ReaderT` is that for instructions that -- have sub-programs, this seems to be the only possible transformer for -- which `reexpressInstrEnv` can be defined (among common monads). E.g. -- the above trick with `runReaderT` doesn't work for `StateT`. -- | Rewrite an instruction changing its \"expression\" sub-structure reexpressInstr :: (Reexpressible i instr (), Monad m) => (forall b . exp1 b -> ProgramT instr '(exp2,fs) m (exp2 b)) -- ^ Conversion of the \"expression\" sub-structure -> i '(ProgramT instr '(exp2,fs) m, '(exp1, fs)) a -> ProgramT instr '(exp2,fs) m a reexpressInstr reexp = flip runReaderT () . reexpressInstrEnv (lift . reexp) . hbimap lift id instance (Reexpressible i1 instr env, Reexpressible i2 instr env) => Reexpressible (i1 :+: i2) instr env where reexpressInstrEnv reexp (Inl i) = reexpressInstrEnv reexp i reexpressInstrEnv reexp (Inr i) = reexpressInstrEnv reexp i -- | Rewrite a program changing its expression type (assuming that the second -- sub-structure of the instruction type represents expressions) -- -- Conversion of expressions is done in the target monad, so pure expressions -- are allowed to expand to monadic code. This can be used e.g. to \"compile\" -- complex expressions into simple expressions with supporting monadic code. reexpress :: (Reexpressible instr1 instr2 (), Monad m) => (forall b . exp1 b -> ProgramT instr2 '(exp2,fs) m (exp2 b)) -- ^ Conversion of expressions -> ProgramT instr1 '(exp1,fs) m a -> ProgramT instr2 '(exp2,fs) m a reexpress reexp p = interpretWithMonadT (reexpressInstr reexp) lift p -- | Rewrite a program changing its expression type (assuming that the second -- sub-structure of the instruction type represents expressions) -- -- Conversion of expressions is done in the target monad, so pure expressions -- are allowed to expand to monadic code. This can be used e.g. to \"compile\" -- complex expressions into simple expressions with supporting monadic code. reexpressEnv :: (Reexpressible instr1 instr2 env, Monad m) => ( forall b . exp1 b -> ReaderT env (ProgramT instr2 '(exp2,fs) m) (exp2 b) ) -- ^ Conversion of expressions -> ProgramT instr1 '(exp1,fs) m a -> ReaderT env (ProgramT instr2 '(exp2,fs) m) a reexpressEnv reexp p = interpretWithMonadT (reexpressInstrEnv reexp) (lift . lift) p