Taking advantage of type synonyms in monad-control
Published on
Bas van Dijk has recently released monad-control-1.0.0.0, the main purpose of which is to replace associated data types with associated type synonyms. The change caused minor breakages here and there, so people might wonder whether and why it was worth it. Let me show you a simple example that demonstrates the difference.
Let’s say we are writing a web application. wai defines an application as
type Application =
Request ->
Response -> IO ResponseReceived) ->
(IO ResponseReceived
Our web app will need a database connection, which we’ll pass using
the ReaderT
transformer:
type ApplicationM m =
Request ->
Response -> m ResponseReceived) ->
(ResponseReceived
m
myApp :: ApplicationM (ReaderT DbConnection IO)
However, warp
can only run an Application
, not
ApplicationM
:
run :: Port -> Application -> IO ()
Can we build
runM :: Port -> ApplicationM m -> m ()
on top of the
simple run
function? Solving the problems like this one is
exactly the purpose of monad-control.
Here’s how such a function might look like:
runM :: (MonadBaseControl IO m)
=> Port -> ApplicationM m -> m ()
= do
runM port app $ \runInIO ->
liftBaseWith $ \req ack -> runInIO $ app req (liftBase . ack) run port
What’s going on here? liftBaseWith
, like
liftM
or liftBase
, allows to run a primitive
monadic action in a complex monad stack. The difference is that it also
gives us a function, here named runInIO
, which lets to
“lower” complex actions to primitive ones. Here we use
runInIO
to translate the return value of our app,
m ()
, into a basic IO ()
value that the
run
function can digest.
All is well, except…
Could not deduce (StM m ResponseReceived ~ ResponseReceived)
Expected type: IO ResponseReceived
Actual type: IO (StM m ResponseReceived)
Relevant bindings include
runInIO :: RunInBase m IO
In the expression: runInIO $ app req (liftBase . ack)
The type of runInIO
is
forall a . m a -> IO (StM m a)
(a.k.a.
RunInBase m IO
), while we would like a simple
forall a . m a -> IO a
. The purpose of StM
is to encompass any “output effects”, such as state or error.
In our case, we don’t have any “output effects” (nor would we be
allowed to), so
StM (ReaderT DbConnection IO) ResponseReceived
is really
isomorphic to ResponseReceived
.
In monad-control 0.x, StM
used to be an associated data
family, and its constructors for the standard monad transformers were
hidden. Even though we knew that the above two types were isomorphic, we
still couldn’t resolve the error nicely.
Not anymore! Since in monad-control 1.0 StM
is an
associated type synonym,
StM (ReaderT DbConnection IO) ResponseReceived
and
ResponseReceived
are not just hypothetically isomorphic;
they are literally the same type. After we add the corresponding
equality constraint to runM
runM :: (MonadBaseControl IO m, StM m ResponseReceived ~ ResponseReceived)
=> Port -> ApplicationM m -> m ()
our app compiles!
This example is not just an isolated case. The general problem with monad-control is that it is all too easy to discard the output effects as Edward Yang shows.
Monads for which StM m a ~ a
provide a “safe subset” of
monad-control. Let’s call them “stateless” monads.
Previously, it was hard to tell apart stateless and stateful monads because the output effects or absence thereof hid behind the opaque StM data family.
Now not only is it transparent when the output effects are absent, but we can actually encode that fact right in the type system! As an example, Mitsutoshi Aoe and I are experimenting with a safe lifted async module.
One may wonder if this subset is too boring, since it only includes
monads that are isomorphic to a reader transformer over the base monad.
While that is technically true, there are a lot of things you can do
with a reader. The ZoomT
and CustomWriterT
transformers that I described in another
article, as well as the Proxied
transformer they’re
based upon, are stateless and thus safe to use with monad-control.
Another common example of a stateless monad transformer is a
ReaderT
holding a reference (such as IORef
,
TVar
, or MVar
). This is commonly used to
maintain a shared state in multithreaded server applications, so it is
fortunate that we can fork threads and catch exceptions inside such
monads.