A continuation of a previous page on extensible-effects.
Oleg suggested to me the following example which shows how easily one can abstract the client in the Extensible Effects framework:
c5 = runTrace $ runReader (loop =<< runC (th client)) (10::Int)
where loop (Y x k) = trace (show (x::Int)) >> local (+(1::Int)) (k ()) >>= loop
loop Done = trace "Done"
-- cl, client, ay are monomorphic bindings
client = ay >> ay
ay = ask >>= \x -> yield (x::Int)
-- There is no polymorphic recursion here
th cl = do
cl
v <- ask
(if v > (20::Int) then id else local (+(5::Int))) cl
if v > (20::Int) then return () else local (+(10::Int)) (th cl)
{-
The expected result is:
10
11
16
16
21
21
21
21
Done
-}
The same result can be achieved with the MTL, but the CoT type needs
a MonadFunctor instance so that we can use hoist. I don’t know if
CoT as implemented via ContT can be given such an instance. As
such I implemened my own version like this
data Step y m a = Done a | Yield y (CoT y m a)
data CoT y m a = CoT (m (Step y m a))
and then we can reimplement c5 straightforwardly
c5 :: IO ()
c5 = runReaderT (loop =<< runC (th client)) (10::Int)
where loop (Yield x k) = (liftIO . print) (show (x::Int)) >> local (+(1::Int)) (runC k) >>= loop
loop (Done _) = (liftIO . print) "Done"
-- Type signatures are not required, but I will
-- say what they are anyway.
-- client :: (MonadCo r m, MonadReader r m) => m ()
client = ay >> ay
-- ay :: (MonadCo r m, MonadReader r m) => m ()
ay = ask >>= yieldG
-- th :: (Monad m, MFunctor t, Monad (t (ReaderT Int m)),
-- MonadReader Int (t (ReaderT Int m)))
-- => t (ReaderT Int m) () -> t (ReaderT Int m) ()
th cl = do
cl
v <- ask
(if v > (20::Int) then id else hoist (localLocal' (+(5::Int)))) cl
if v > (20::Int) then return () else hoist (localLocal' (+(10::Int))) (th cl)
The reimplementation is remarkably similar to the original with just
some hoists added. Running it gives exactly the result desired
GHCi> c5
"10"
"11"
"16"
"16"
"21"
"21"
"21"
"21"
"Done"
Note: Later Oleg pointed out to me that the definition of th above
only works on transformer stacks two deep, where the second is
ReaderT. It seems to me that any more deeply nested stack can be
converted to one where the ReaderT occurs at the second layer, using,
for example
ComposeTrans t1 t2 m = ComposeTrans (t1 (t2 m))
and if we want the ReaderT to occur at the first layer, we can just wrap it in IdentityT instead.
However, this introduces a lot more awkwardness which Extensible Effects doesn’t have, the latter being designed exactly to avoid this kind of thing. So perhaps this is a good example to demonstrate exactly what problem it is that Extensible Effects solves.
The concept of MFunctor and its method hoist seem to play a
crucial role here. In the case of a free monad I would guess that the
MFunctor instance comes for free, i.e. free monads always support
hoist. This seems to be a crucial missing element from the MTL to
allow effects to work with each other in a generic manner. I need to
think about this more …