Yesod’s Monads

As you’ve read through this book, there have been a number of monads which have appeared: Handler, Widget and YesodDB (for Persistent). As with most monads, each one provides some specific functionality: Handler gives access to the request and allows you to send responses, a Widget contains HTML, CSS, and Javascript, and YesodDB lets you make database queries. In Model-View-Controller (MVC) terms, we could consider YesodDB to be the model, Widget to be the view, and Handler to be the controller.

So far, we’ve presented some very straight-forward ways to use these monads: your main handler will run in Handler, using runDB to execute a YesodDB query, and defaultLayout to return a Widget, which in turn was created by calls to toWidget.

However, if we have a deeper understanding of these types, we can achieve some fancier results.

Monad Transformers

Shrek- more or less

Monads are like onions. Monads are not like cakes.

Before we get into the heart of Yesod’s monads, we need to understand a bit about monad transformers. (If you already know all about monad transformers, you can likely skip this section.) Different monads provide different functionality: Reader allows read-only access to some piece of data throughout a computation, Error allows you to short-circuit computations, and so on.

Often times, however, you would like to be able to combine a few of these features together. After all, why not have a computation with read-only access to some settings variable, that could error out at any time? One approach to this would be to write a new monad like ReaderError, but this has the obvious downside of exponential complexity: you’ll need to write a new monad for every single possible combination.

Instead, we have monad transformers. In addition to Reader, we have ReaderT, which adds reader functionality to any other monad. So we could represent our ReaderError as (conceptually):

  1. type ReaderError = ReaderT Error

In order to access our settings variable, we can use the ask function. But what about short-circuiting a computation? We’d like to use throwError, but that won’t exactly work. Instead, we need to lift our call into the next monad up. In other words:

  1. throwError :: errValue -> Error
  2. lift . throwError :: errValue -> ReaderT Error

There are a few things you should pick up here:

  • A transformer can be used to add functionality to an existing monad.

  • A transformer must always wrap around an existing monad.

  • The functionality available in a wrapped monad will be dependent not only on the monad transformer, but also on the inner monad that is being wrapped.

A great example of that last point is the IO monad. No matter how many layers of transformers you have around an IO, there’s still an IO at the core, meaning you can perform I/O in any of these monad transformer stacks. You’ll often see code that looks like liftIO $ putStrLn "Hello There!".

The Three Transformers

In earlier versions of Yesod, Handler and Widget were far more magical and scary. Since version 1.2, things are much simplified. So if you remember reading some scary stuff about fake transformers and subsite parameters, rest assured: you haven’t gone crazy, things have actually changed a bit. The story with persistent is likewise much simpler.

We’ve already discussed two of our transformers previously: Handler and Widget. Remember that these are each application-specific synonyms for the more generic HandlerT and WidgetT. Each of those transformers takes two type parameters: your foundation data type, and a base monad. The most commonly used base monad is IO.

In persistent, we have a typeclass called PersistStore. This typeclass defines all of the primitive operations you can perform on a database, like get. There are instances of this typeclass for each database backend supported by persistent. For example, for SQL databases, there is a datatype called SqlBackend. We then use a standard ReaderT transformer to provide that SqlBackend value to all of our operations. This means that you can run a SQL database with any underlying monad which is an instance of MonadIO. The takeaway here is that we can layer our Persistent transformer on top of Handler or Widget.

In order to make it simpler to refer to the relevant Persistent transformer, the yesod-persistent package defines the YesodPersistBackend associated type. For example, if I have a site called MyApp and it uses SQL, I would define something like type instance YesodPersistBackend MyApp = SqlBackend. And for more convenience, we have a type synonym called YesodDB which is defined as:

  1. type YesodDB site = ReaderT (YesodPersistBackend site) (HandlerT site IO)

Our database actions will then have types that look like YesodDB MyApp SomeResult. In order to run these, we can use the standard Persistent unwrap functions (like runSqlPool) to run the action and get back a normal Handler. To automate this, we provide the runDB function. Putting it all together, we can now run database actions inside our handlers.

Most of the time in Yesod code, and especially thus far in this book, widgets have been treated as actionless containers that simply combine together HTML, CSS and Javascript. But in reality, a Widget can do anything that a Handler can do, by using the handlerToWidget function. So for example, you can run database queries inside a Widget by using something like handlerToWidget . runDB.

Example: Database-driven navbar

Let’s put some of this new knowledge into action. We want to create a Widget that generates its output based on the contents of the database. Previously, our approach would have been to load up the data in a Handler, and then pass that data into a Widget. Now, we’ll do the loading of data in the Widget itself. This is a boon for modularity, as this Widget can be used in any Handler we want, without any need to pass in the database contents.

  1. {-# LANGUAGE FlexibleContexts #-}
  2. {-# LANGUAGE GADTs #-}
  3. {-# LANGUAGE GeneralizedNewtypeDeriving #-}
  4. {-# LANGUAGE MultiParamTypeClasses #-}
  5. {-# LANGUAGE OverloadedStrings #-}
  6. {-# LANGUAGE QuasiQuotes #-}
  7. {-# LANGUAGE TemplateHaskell #-}
  8. {-# LANGUAGE TypeFamilies #-}
  9. import Control.Monad.Logger (runNoLoggingT)
  10. import Data.Text (Text)
  11. import Data.Time
  12. import Database.Persist.Sqlite
  13. import Yesod
  14. share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
  15. Link
  16. title Text
  17. url Text
  18. added UTCTime
  19. |]
  20. data App = App ConnectionPool
  21. mkYesod "App" [parseRoutes|
  22. / HomeR GET
  23. /add-link AddLinkR POST
  24. |]
  25. instance Yesod App
  26. instance RenderMessage App FormMessage where
  27. renderMessage _ _ = defaultFormMessage
  28. instance YesodPersist App where
  29. type YesodPersistBackend App = SqlBackend
  30. runDB db = do
  31. App pool <- getYesod
  32. runSqlPool db pool
  33. getHomeR :: Handler Html
  34. getHomeR = defaultLayout
  35. [whamlet|
  36. <form method=post action=@{AddLinkR}>
  37. <p>
  38. Add a new link to
  39. <input type=url name=url value=http://>
  40. titled
  41. <input type=text name=title>
  42. <input type=submit value="Add link">
  43. <h2>Existing links
  44. ^{existingLinks}
  45. |]
  46. existingLinks :: Widget
  47. existingLinks = do
  48. links <- handlerToWidget $ runDB $ selectList [] [LimitTo 5, Desc LinkAdded]
  49. [whamlet|
  50. <ul>
  51. $forall Entity _ link <- links
  52. <li>
  53. <a href=#{linkUrl link}>#{linkTitle link}
  54. |]
  55. postAddLinkR :: Handler ()
  56. postAddLinkR = do
  57. url <- runInputPost $ ireq urlField "url"
  58. title <- runInputPost $ ireq textField "title"
  59. now <- liftIO getCurrentTime
  60. runDB $ insert $ Link title url now
  61. setMessage "Link added"
  62. redirect HomeR
  63. main :: IO ()
  64. main = runNoLoggingT $ withSqlitePool "links.db3" 10 $ \pool -> liftIO $ do
  65. runSqlPersistMPool (runMigration migrateAll) pool
  66. warp 3000 $ App pool

Pay attention in particular to the existingLinks function. Notice how all we needed to do was apply handlerToWidget . runDB to a normal database action. And from within getHomeR, we treated existingLinks like any ordinary Widget, no special parameters at all. See the figure for the output of this app.

Screenshot of the navbar

Yesod’s Monads - 图1

Example: Request information

Likewise, you can get request information inside a Widget. Here we can determine the sort order of a list based on a GET parameter.

  1. {-# LANGUAGE MultiParamTypeClasses #-}
  2. {-# LANGUAGE OverloadedStrings #-}
  3. {-# LANGUAGE QuasiQuotes #-}
  4. {-# LANGUAGE TemplateHaskell #-}
  5. {-# LANGUAGE TypeFamilies #-}
  6. import Data.List (sortOn)
  7. import Data.Text (Text)
  8. import Yesod
  9. data Person = Person
  10. { personName :: Text
  11. , personAge :: Int
  12. }
  13. people :: [Person]
  14. people =
  15. [ Person "Miriam" 25
  16. , Person "Eliezer" 3
  17. , Person "Michael" 26
  18. , Person "Gavriella" 1
  19. ]
  20. data App = App
  21. mkYesod "App" [parseRoutes|
  22. / HomeR GET
  23. |]
  24. instance Yesod App
  25. instance RenderMessage App FormMessage where
  26. renderMessage _ _ = defaultFormMessage
  27. getHomeR :: Handler Html
  28. getHomeR = defaultLayout
  29. [whamlet|
  30. <p>
  31. <a href="?sort=name">Sort by name
  32. |
  33. <a href="?sort=age">Sort by age
  34. |
  35. <a href="?">No sort
  36. ^{showPeople}
  37. |]
  38. showPeople :: Widget
  39. showPeople = do
  40. msort <- runInputGet $ iopt textField "sort"
  41. let people' =
  42. case msort of
  43. Just "name" -> sortOn personName people
  44. Just "age" -> sortOn personAge people
  45. _ -> people
  46. [whamlet|
  47. <dl>
  48. $forall person <- people'
  49. <dt>#{personName person}
  50. <dd>#{show $ personAge person}
  51. |]
  52. main :: IO ()
  53. main = warp 3000 App

Notice that in this case, we didn’t even have to call handlerToWidget. The reason is that a number of the functions included in Yesod automatically work for both Handler and Widget, by means of the MonadHandler typeclass. In fact, MonadHandler will allow these functions to be “autolifted” through many common monad transformers.

But if you want to, you can wrap up the call to runInputGet above using handlerToWidget, and everything will work the same.

Performance and error messages

You can consider this section extra credit. It gets into some of the design motivation behind Yesod, which isn’t necessary for usage of Yesod.

At this point, you may be just a bit confused. As I mentioned above, the Widget synonym uses IO as its base monad, not Handler. So how can Widget perform Handler actions? And why not just make Widget a transformer on top of Handler, and then use lift instead of this special handlerToWidget? And finally, I mentioned that Widget and Handler were both instances of MonadResource. If you’re familiar with MonadResource, you may be wondering why ResourceT doesn’t appear in the monad transformer stack.

The fact of the matter is, there’s a much simpler (in terms of implementation) approach we could take for all of these monad transformers. Handler could be a transformer on top of ResourceT IO instead of just IO, which would be a bit more accurate. And Widget could be layered on top of Handler. The end result would look something like this:

  1. type Handler = HandlerT App (ResourceT IO)
  2. type Widget = WidgetT App (HandlerT App (ResourceT IO))

Doesn’t look too bad, especially since you mostly deal with the more friendly type synonyms instead of directly with the transformer types. The problem is that any time those underlying transformers leak out, these larger type signatures can be incredibly confusing. And the most common time for them to leak out is in error messages, when you’re probably already pretty confused! (Another time is when working on subsites, which happens to be confusing too.)

One other concern is that each monad transformer layer does add some amount of a performance penalty. This will probably be negligible compared to the I/O you’ll be performing, but the overhead is there.

So instead of having properly layered transformers, we flatten out each of HandlerT and WidgetT into a one-level transformer. Here’s a high-level overview of the approach we use:

  • HandlerT is really just a ReaderT monad. (We give it a different name to make error messages clearer.) This is a reader for the HandlerData type, which contains request information and some other immutable contents.

  • In addition, HandlerData holds an IORef to a GHState (badly named for historical reasons), which holds some data which can be mutated during the course of a handler (e.g., session variables). The reason we use an IORef instead of a StateT kind of approach is that IORef will maintain the mutated state even if a runtime exception is thrown.

  • The ResourceT monad transformer is essentially a ReaderT holding onto an IORef. This IORef contains the information on all cleanup actions that must be performed. (This is called InternalState.) Instead of having a separate transformer layer to hold onto that reference, we hold onto the reference ourself in HandlerData. (And yes, the reson for an IORef here is also for runtime exceptions.)

  • A WidgetT is essentially just a WriterT on top of everything that a HandlerT does. But since HandlerT is just a ReaderT, we can easily compress the two aspects into a single transformer, which looks something like newtype WidgetT site m a = WidgetT (HandlerData → m (a, WidgetData)).

If you want to understand this more, please have a look at the definitions of HandlerT and WidgetT in Yesod.Core.Types.

Adding a new monad transformer

At times, you’ll want to add your own monad transformer in part of your application. As a motivating example, let’s consider the monadcryptorandom package from Hackage, which defines both a MonadCRandom typeclass for monads which allow generating cryptographically-secure random values, and CRandT as a concrete instance of that typeclass. You would like to write some code that generates a random bytestring, e.g.:

  1. import Control.Monad.CryptoRandom
  2. import Data.ByteString.Base16 (encode)
  3. import Data.Text.Encoding (decodeUtf8)
  4. getHomeR = do
  5. randomBS <- getBytes 128
  6. defaultLayout
  7. [whamlet|
  8. <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
  9. |]

However, this results in an error message along the lines of:

  1. No instance for (MonadCRandom e0 (HandlerT App IO))
  2. arising from a use of getBytes
  3. In a stmt of a 'do' block: randomBS <- getBytes 128

How do we get such an instance? One approach is to simply use the CRandT monad transformer when we call getBytes. A complete example of doing so would be:

  1. {-# LANGUAGE OverloadedStrings #-}
  2. {-# LANGUAGE QuasiQuotes #-}
  3. {-# LANGUAGE TemplateHaskell #-}
  4. {-# LANGUAGE TypeFamilies #-}
  5. import Yesod
  6. import Crypto.Random (SystemRandom, newGenIO)
  7. import Control.Monad.CryptoRandom
  8. import Data.ByteString.Base16 (encode)
  9. import Data.Text.Encoding (decodeUtf8)
  10. data App = App
  11. mkYesod "App" [parseRoutes|
  12. / HomeR GET
  13. |]
  14. instance Yesod App
  15. getHomeR :: Handler Html
  16. getHomeR = do
  17. gen <- liftIO newGenIO
  18. eres <- evalCRandT (getBytes 16) (gen :: SystemRandom)
  19. randomBS <-
  20. case eres of
  21. Left e -> error $ show (e :: GenError)
  22. Right gen -> return gen
  23. defaultLayout
  24. [whamlet|
  25. <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
  26. |]
  27. main :: IO ()
  28. main = warp 3000 App

Note that what we’re doing is layering the CRandT transformer on top of the HandlerT transformer. It does not work to do things the other way around: Yesod itself would ultimately have to unwrap the CRandT transformer, and it has no knowledge of how to do so. Notice that this is the same approach we take with Persistent: its transformer goes on top of HandlerT.

But there are two downsides to this approach:

  1. It requires you to jump into this alternate monad each time you want to work with random values.

  2. It’s inefficient: you need to create a new random seed each time you enter this other monad.

The second point could be worked around by storing the random seed in the foundation datatype, in a mutable reference like an IORef, and then atomically sampling it each time we enter the CRandT transformer. But we can even go a step further, and use this trick to make our Handler monad itself an instance of MonadCRandom! Let’s look at the code, which is in fact a bit involved:

  1. {-# LANGUAGE FlexibleInstances #-}
  2. {-# LANGUAGE MultiParamTypeClasses #-}
  3. {-# LANGUAGE OverloadedStrings #-}
  4. {-# LANGUAGE QuasiQuotes #-}
  5. {-# LANGUAGE TemplateHaskell #-}
  6. {-# LANGUAGE TypeFamilies #-}
  7. {-# LANGUAGE TypeSynonymInstances #-}
  8. import Control.Monad (join)
  9. import Control.Monad.Catch (throwM)
  10. import Control.Monad.CryptoRandom
  11. import Control.Monad.Error.Class (MonadError (..))
  12. import Crypto.Random (SystemRandom, newGenIO)
  13. import Data.ByteString.Base16 (encode)
  14. import Data.IORef
  15. import Data.Text.Encoding (decodeUtf8)
  16. import UnliftIO.Exception (catch)
  17. import Yesod
  18. data App = App
  19. { randGen :: IORef SystemRandom
  20. }
  21. mkYesod "App" [parseRoutes|
  22. / HomeR GET
  23. |]
  24. instance Yesod App
  25. getHomeR :: Handler Html
  26. getHomeR = do
  27. randomBS <- getBytes 16
  28. defaultLayout
  29. [whamlet|
  30. <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
  31. |]
  32. instance MonadError GenError Handler where
  33. throwError = throwM
  34. catchError = catch
  35. instance MonadCRandom GenError Handler where
  36. getCRandom = wrap crandom
  37. {-# INLINE getCRandom #-}
  38. getBytes i = wrap (genBytes i)
  39. {-# INLINE getBytes #-}
  40. getBytesWithEntropy i e = wrap (genBytesWithEntropy i e)
  41. {-# INLINE getBytesWithEntropy #-}
  42. doReseed bs = do
  43. genRef <- fmap randGen getYesod
  44. join $ liftIO $ atomicModifyIORef genRef $ \gen ->
  45. case reseed bs gen of
  46. Left e -> (gen, throwM e)
  47. Right gen' -> (gen', return ())
  48. {-# INLINE doReseed #-}
  49. wrap :: (SystemRandom -> Either GenError (a, SystemRandom)) -> Handler a
  50. wrap f = do
  51. genRef <- fmap randGen getYesod
  52. join $ liftIO $ atomicModifyIORef genRef $ \gen ->
  53. case f gen of
  54. Left e -> (gen, throwM e)
  55. Right (x, gen') -> (gen', return x)
  56. main :: IO ()
  57. main = do
  58. gen <- newGenIO
  59. genRef <- newIORef gen
  60. warp 3000 App
  61. { randGen = genRef
  62. }

This really comes down to a few different concepts:

  1. We modify the App datatype to have a field for an IORef SystemRandom.

  2. Similarly, we modify the main function to generate an IORef SystemRandom.

  3. Our getHomeR function became a lot simpler: we can now simply call getBytes without playing with transformers.

  4. However, we have gained some complexity in needing a MonadCRandom instance. Since this is a book on Yesod, and not on monadcryptorandom, I’m not going to go into details on this instance, but I encourage you to inspect it, and if you’re interested, compare it to the instance for CRandT.

Hopefully, this helps get across an important point: the power of the HandlerT transformer. By just providing you with a readable environment, you’re able to recreate a StateT transformer by relying on mutable references. In fact, if you rely on the underlying IO monad for runtime exceptions, you can implement most cases of ReaderT, WriterT, StateT, and ErrorT with this abstraction.

Summary

If you completely ignore this chapter, you’ll still be able to use Yesod to great benefit. The advantage of understanding how Yesod’s monads interact is to be able to produce cleaner, more modular code. Being able to perform arbitrary actions in a Widget can be a powerful tool, and understanding how Persistent and your Handler code interact can help you make more informed design decisions in your app.