|
1 | 1 | --- |
2 | 2 | layout: tutorial |
3 | 3 | 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' ] |
5 | 5 | title: 3. Getting to know Processes |
6 | 6 | --- |
7 | 7 |
|
@@ -366,10 +366,187 @@ process. The `ProcessInfo` type it returns contains the local node id and a list |
366 | 366 | registered names, monitors and links for the process. The call returns `Nothing` if the |
367 | 367 | process in question is not alive. |
368 | 368 |
|
| 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 | + |
369 | 534 | [1]: hackage.haskell.org/package/distributed-process/docs/Control-Distributed-Process.html#v:receiveWait |
370 | 535 | [2]: hackage.haskell.org/package/distributed-process/docs/Control-Distributed-Process.html#v:expect |
371 | 536 | [3]: http://hackage.haskell.org/package/distributed-process-0.4.2/docs/Control-Distributed-Process.html#v:match |
372 | 537 | [4]: /static/semantics.pdf |
373 | 538 | [5]: http://hackage.haskell.org/package/distributed-process-0.4.2/docs/Control-Distributed-Process.html#v:exit |
374 | 539 | [6]: http://hackage.haskell.org/package/distributed-process-0.4.2/docs/Control-Distributed-Process.html#v:kill |
375 | 540 | [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