Skip to content

Commit bc39acc

Browse files
committed
Merge pull request #10 from jeremyjh/monad-control-tutorial
add monad-control tutorial
2 parents 2e09e91 + 0866763 commit bc39acc

File tree

1 file changed

+178
-1
lines changed

1 file changed

+178
-1
lines changed

tutorials/3ch.md

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
layout: tutorial
33
categories: tutorial
4-
sections: ['Message Ordering', 'Selective Receive', 'Advanced Mailbox Processing', 'Typed Channels', 'Process Lifetime', 'Monitoring And Linking', 'Getting Process Info']
4+
sections: ['Message Ordering', 'Selective Receive', 'Advanced Mailbox Processing', 'Typed Channels', 'Process Lifetime', 'Monitoring And Linking', 'Getting Process Info','Monad Transformer Stacks' ]
55
title: 3. Getting to know Processes
66
---
77

@@ -366,10 +366,187 @@ process. The `ProcessInfo` type it returns contains the local node id and a list
366366
registered names, monitors and links for the process. The call returns `Nothing` if the
367367
process in question is not alive.
368368

369+
### Monad Transformer Stacks
370+
371+
It is not generally necessary, but it may be convenient in your application to use a
372+
custom monad transformer stack with the Process monad at the bottom. For example,
373+
you may have decided that in various places in your application you will make calls to
374+
a network database. You may create a data access module, and it will need configuration information available to it in
375+
order to connect to the database server. A ReaderT can be a nice way to make
376+
configuration data available throughout an application without
377+
schlepping it around by hand.
378+
379+
This example is a bit contrived and over-simplified but
380+
illustrates the concept. Consider the `fetchUser` function below, it runs in the `AppProcess`
381+
monad which provides the configuration settings required to connect to the database:
382+
383+
{% highlight haskell %}
384+
385+
import Data.ByteString (ByteString)
386+
import Control.Monad.Reader
387+
388+
-- imagine we have some database library
389+
import Database.Imaginary as DB
390+
391+
data AppConfig = AppConfig {dbHost :: String, dbUser :: String}
392+
393+
type AppProcess = ReaderT AppConfig Process
394+
395+
data User = User {userEmail :: String}
396+
397+
-- Perform a user lookup using our custom app context
398+
fetchUser :: String -> AppProcess (Maybe User)
399+
fetchUser email = do
400+
db <- openDB
401+
user <- liftIO $ DB.query db email
402+
closeDB db
403+
return user
404+
405+
openDB :: AppProcess DB.Connection
406+
openDB = do
407+
AppConfig host user <- ask
408+
liftIO $ DB.connect host user
409+
410+
closeDB :: DB.Connection -> AppProcess ()
411+
closeDB db = liftIO (DB.close db)
412+
413+
{% endhighlight %}
414+
415+
So this would mostly work but it is not complete. What happens if an exception
416+
is thrown by the `query` function? Your open database handle may not be
417+
closed. Typically we manage this with the [bracket][brkt] function.
418+
419+
In the base library, [bracket][brkt] is defined in Control.Exception with this signature:
420+
421+
{% highlight haskell %}
422+
423+
bracket :: IO a --^ computation to run first ("acquire resource")
424+
-> (a -> IO b) --^ computation to run last ("release resource")
425+
-> (a -> IO c) --^ computation to run in-between
426+
-> IO c
427+
428+
{% endhighlight %}
429+
430+
Great! We pass an IO action that acquires a resource; `bracket` passes that
431+
resource to a function which takes the resource and runs another action.
432+
We also provide a release function which `bracket` is guaranteed to run
433+
even if the primary action raises an exception.
434+
435+
436+
Unfortunately, we cannot directly use `bracket` in our
437+
`fetchUser` function: openDB (resource acquisition) runs in the `AppProcess`
438+
monad. If our functions ran in IO, we could lift the entire bracket computation into
439+
our monad transformer stack with liftIO; but we cannot do that for the computations
440+
*passed* to bracket.
441+
442+
It is perfectly possible to write our own bracket; `distributed-process` does this
443+
for the `Process` monad (which is itself a newtyped ReaderT stack). Here is how that is done:
444+
445+
446+
{% highlight haskell %}
447+
-- | Lift 'Control.Exception.bracket'
448+
bracket :: Process a -> (a -> Process b) -> (a -> Process c) -> Process c
449+
bracket before after thing =
450+
mask $ \restore -> do
451+
a <- before
452+
r <- restore (thing a) `onException` after a
453+
_ <- after a
454+
return r
455+
456+
mask :: ((forall a. Process a -> Process a) -> Process b) -> Process b
457+
mask p = do
458+
lproc <- ask
459+
liftIO $ Ex.mask $ \restore ->
460+
runLocalProcess lproc (p (liftRestore restore))
461+
where
462+
liftRestore :: (forall a. IO a -> IO a)
463+
-> (forall a. Process a -> Process a)
464+
liftRestore restoreIO = \p2 -> do
465+
ourLocalProc <- ask
466+
liftIO $ restoreIO $ runLocalProcess ourLocalProc p2
467+
468+
-- | Lift 'Control.Exception.onException'
469+
onException :: Process a -> Process b -> Process a
470+
onException p what = p `catch` \e -> do _ <- what
471+
liftIO $ throwIO (e :: SomeException)
472+
{% endhighlight %}
473+
474+
`distributed-process` needs to do this sort of thing to keep its dependency
475+
list small, but do we really want to write this for every transformer stack
476+
we use in our own applications? No! And we do not have to, thanks to
477+
the [monad-control][mctrl] and [lifted-base][lbase] libraries.
478+
479+
[monad-control][mctrl] provides several typeclasses and helper functions
480+
that make it possible to fully generalize the wrapping/unwrapping required
481+
to keep transformer effects stashed away while actions run in the base monad. Of
482+
most concern to end users of this library are the typeclass [MonadBase][mb] and [MonadBaseControl][mbc].
483+
How it works is beyond the scope of this tutorial, but there is an excellent and thorough
484+
explanation written by Michael Snoyman which is available [here][mctrlt].
485+
486+
[lifted-base][lbase] takes advantage of these typeclasses to provide lifted versions of many functions
487+
in the Haskell base library. For example, [Control.Exception.Lifted][lexc] has a definition of
488+
bracket that looks like this:
489+
490+
{% highlight haskell %}
491+
492+
bracket :: MonadBaseControl IO m
493+
=> m a --^ computation to run first ("acquire resource")
494+
-> (a -> m b) --^ computation to run last ("release resource")
495+
-> (a -> m c) --^ computation to run in-between
496+
-> m c
497+
498+
{% endhighlight %}
499+
500+
It is just the same as the version found in base, except it is generalized to work
501+
with actions in any monad that implements [MonadBaseControl IO][mbc]. [monad-control][mctrl] defines
502+
instances for the standard transformers, but that instance requires the base monad
503+
(in this case, `Process`) to also have an instance of these classes.
504+
505+
To address this the [distributed-process-monad-control][dpmc] package
506+
provides orphan instances of the `Process` type for both [MonadBase IO][mb] and [MonadBaseControl IO][mbc].
507+
After importing these, we can rewrite our `fetchUser` function to use the instance of bracket
508+
provided by [lifted-base][lbase].
509+
510+
{% highlight haskell %}
511+
512+
-- ...
513+
import Control.Distributed.Process.MonadBaseControl ()
514+
import Control.Exception.Lifted as Lifted
515+
516+
-- ...
517+
518+
fetchUser :: String -> AppProcess (Maybe User)
519+
fetchUser email =
520+
Lifted.bracket openDB
521+
closeDB
522+
$ \db -> liftIO $ DB.query db email
523+
524+
525+
{% endhighlight %}
526+
527+
[lifted-base][lbase] also provides conveniences like [MVar][lmvar] and other concurrency primitives that
528+
operate in [MonadBase IO][mb]. One benefit here is that your code is not sprinkled with
529+
liftIO; but [MonadBaseControl IO][mbc] also makes things like a lifted [withMVar][lmvar-with] possible - which is
530+
really just a specialization of [bracket][lbrkt]. You will also find lots of other libraries on hackage which
531+
use these instances - at present count there are more than 150 [packages using it][reverse].
532+
533+
369534
[1]: hackage.haskell.org/package/distributed-process/docs/Control-Distributed-Process.html#v:receiveWait
370535
[2]: hackage.haskell.org/package/distributed-process/docs/Control-Distributed-Process.html#v:expect
371536
[3]: http://hackage.haskell.org/package/distributed-process-0.4.2/docs/Control-Distributed-Process.html#v:match
372537
[4]: /static/semantics.pdf
373538
[5]: http://hackage.haskell.org/package/distributed-process-0.4.2/docs/Control-Distributed-Process.html#v:exit
374539
[6]: http://hackage.haskell.org/package/distributed-process-0.4.2/docs/Control-Distributed-Process.html#v:kill
375540
[7]: http://hackage.haskell.org/package/distributed-process-0.4.2/docs/Control-Distributed-Process.html#v:die
541+
[brkt]: http://hackage.haskell.org/package/base-4.6.0.1/docs/Control-Exception.html#v:bracket
542+
[mctrl]: http://hackage.haskell.org/package/monad-control
543+
[mb]: http://hackage.haskell.org/package/transformers-base-0.4.2/docs/Control-Monad-Base.html#t:MonadBase
544+
[mbc]: http://hackage.haskell.org/package/monad-control-0.3.3.0/docs/Control-Monad-Trans-Control.html#t:MonadBaseControl
545+
[lmvar]: http://hackage.haskell.org/package/lifted-base-0.2.2.2/docs/Control-Concurrent-MVar-Lifted.html#t:MVar
546+
[lmvar-with]: http://hackage.haskell.org/package/lifted-base-0.2.2.2/docs/Control-Concurrent-MVar-Lifted.html#v:withMVar
547+
[lbrkt]: http://hackage.haskell.org/package/lifted-base-0.2.2.2/docs/Control-Exception-Lifted.html#v:bracket
548+
[lbase]: http://hackage.haskell.org/package/lifted-base
549+
[dpmc]: http://hackage.haskell.org/package/distributed-process-monad-control
550+
[mctrlt]: http://www.yesodweb.com/book/monad-control
551+
[reverse]: http://packdeps.haskellers.com/reverse/monad-control
552+
[lexc]: http://hackage.haskell.org/package/lifted-base-0.2.2.2/docs/Control-Exception-Lifted.html

0 commit comments

Comments
 (0)