@hackage ytl0.1.0.0

mtl-style transformations for Yesod sites

ytl - Yesod Transformer Library

Yesod is a powerful, type-safe webserver library. At its core, Yesod lets you describe foundation sites which carry the configuration of your webserver, and handlers and widgets which use that foundation to handle requests, generate HTML and CSS, and even access a database.

Haskellers who are familiar with mtl are used to transforming monad stacks, adding or computing monadic features at whim, to introduce wide-ranging effects to a program with a few small, type-safe changes. Unfortunately, Yesod does not support monad transformers - its basic HandlerFor and WidgetFor monads are woven into the server code, and won't allow for other monads to stack on top.

However, a mtl-style approach can be done for Yesod - the secret is, rather than transforming Yesod's HandlerFor and WidgetFor monads, to transform the foundation site itself. The result is ytl - the Yesod transfomer library, which exports a similar API to mtl, but in terms of site transformers.

A site transformer wraps a Yesod foundation site in additional information. Yesod's code is fine with custom foundation types, so the wrapped site can be used in Yesod code as normal - giving additional effects to existing Yesod code for "free".

An example

The ReaderSite transformer adds additional data to a site, similar to how ReaderT adds data to a monad stack. The data accessible by ReaderSite can be accessed through a MonadReader-style API in Yesod's HandlerFor and WidgetFor monads - like this:

useValue
  :: (SiteReader r site)
  => HandlerFor site a
  -> (r -> m site b)
  -> HandlerFor site (a, b)
useValue mArg f = do
  a <- mArg
  r <- ask
  b <- f r
  pure (a, b)

Writing your own transformers

Writing your own ytl site transformers is relatively simple. Define your site wrapper, and some class for its API - in the above example, ReaderSite is the wrapper, and SiteReader is the API class.

Implement the class for the wrapper (duh!). Then, to interact nicely with the other transformers, you'll want to define a lifting instance, which lifts the API implementation through any transformer layers. For ReaderSite, this looks like

instance {-# OVERLAPPABLE #-}
  (SiteTrans t, SiteReader r site) => SiteReader r (t site) where
  ...

Implement RenderRoute, and maybe Yesod

You'll also need to implement RenderRoute for your wrapped site type - make sure to make the route type coercible to the route type of the base site, because ytl relies on them being representation-identical (see SiteCompatible)! This is normally fine, because you don't want to modify the routes anyways.

If your site transformer changes some Yesod behaviour - like logging, middleware, etc. - you need to also implement Yesod for your wrapped site type. ytl comes with some Template Haskell helpers for writing Yesod implementations that pass through to the base site most of the time; so if you just want to override how logging works, you can ignore the other Yesod methods and they'll be implemented for you.

Why isn't my translation of this mtl type working?

Remember that, in Yesod's handlers and widgets, the site is in reader position - it gets passed into Yesod functions. That means that the site type is basically contravariant (as evidenced by withSiteT) - so HandlerFor and WidgetFor are an awful lot like profunctors. That means that you can't translate mtl-style code directly - you have to consider the difference in data flow.

For example, ReaderSite is not an exact copy of ReaderT: ReaderT takes the read value as an argument:

data ReaderT m a = ReaderT (r -> m a)

But for Yesod, the site is already in reader position - taking r as an argument wouldn't have the right semantics. Instead, ReaderSite includes r alongside the site:

data ReaderSite r site = ReaderSite r site

That looks very different. However, they end up being very similar in use:

runReaderT :: ReaderT r m a -> r -> m a

runReaderSite :: HandlerFor (ReaderSite r site) a -> r -> HandlerFor site a