Your type looks a little bit like Apfelmus's operational
monad, also known as the Freer
monad:
data Program inst a where
Return :: a -> Program inst a
Bind :: inst a -> (a -> Program inst b) -> Program inst b
instance Monad (Program inst) where
return = Return
Return x >>= f = f x
Bind i next >>= f = Bind i (fmap (>>= f) next)
-- plus the usual Functor and Applicative boilerplate
Program :: (* -> *) -> * -> *
represents a sequence of instructions inst
, which use their type parameter to indicate the "return type" of running that instruction in an interpreter. The Bind
constructor takes an instruction and a continuation which can be run after the result of the instruction has been received from the interpreter. Note how a
is existentially quantified, reflecting the fact that the types of all the intermediate steps in a computation are not relevant to the overall type.
The important difference between Program
and your type is that the type of the response is determined by the instruction, rather than being fixed over the whole computation. This allows us to make more fine-grained guarantees about the response that each request expects to provoke.
For example, here's the state monad written as a Program
:
data StateI s r where
Get :: StateI s s
Put :: s -> StateI s ()
type State s = Program (StateI s)
get :: State s s
get = Bind Get Return
put :: s -> State s ()
put s = Bind (Put s) Return
modify :: (s -> s) -> State s ()
modify f = do
x <- get
put (f x)
runState :: State s a -> s -> (s, a)
runState (Return x) s = (s, x)
runState (Bind Get next) s = runState (next s) s
runState (Bind (Put s) next) _ = runState (next ()) s
The co-Yoneda lemma tells us that Program
is isomorphic to Free
. Intuitively, it's a free monad based on ->
's Functor
instance. For certain operations like left-associative binds, Program
can be more efficient than Free
, because its >>=
is based on function composition, rather than possibly-expensively fmap
ping an arbitrary Functor
.