Overloading Monads for Html Deployment

It can be tedious migrating an app. I often ask myself, "did I use an absolute or a relative path?" "Did I use the correct hostname?" "Am I going to inline any JavaScript or Css, or use an external link?" Luckilly, a lot of these questions can be answered with algebraic compositon - in this case, overloaded monads.

Introduction

Before I stub my toe, I want to express what I'm trying to do here: make Html templating easier. If you want nothing to do with Html, please close this page :)

Haskell has a few competing Html template libraries:

  • Blaze - the most popular alternative
  • Hamlet - really just Blaze under the hood
  • Lucid - an alternative to Blaze, with a more correct Monad implementation

Each of these are very nice for getting the "job done" - any of them can bring you simple Html. However, there are two common pitfalls when attempting to write migratable Html:

  • String-based routing
  • Node-based imports

String-based routing - simple to understand: links (either internal or external) will make their reference only with string literals, for example: <a href="foo.com/bar">. The concerns with this are maintenance and isolation of bugs - when I move my app from foo.com to foo2.com, will all the links work correctly?

Node-based imports - a little more subtle flaw: imagine I want to include Css in my page. I have two options - I can use a <link> tag to include an external resource, or I can make it "inline" in my page - between two <style></style> tags. Both of which are valuable options to keep available. The issues with this is, again, maintenance and isolation of bugs. Did I use a CDN, or did I host them locally?

These issues can be relieved with a simple trick - enriching your templating system with monads, overloading the monads with typeclasses, then implementing the intended behavior for each monad as an instance. You can then change application-wide behavior by changing your type signature.

Before I go any further, Yesod already has stringless, labelled routes. If you're using Yesod, but for some aweful reason you want string routes, I've included a function in the library. See below.

Moving On

Lucid actually has an advantage over Blaze - it has correct algebraic structure. More importantly, Lucid is a Monad Transformer, which (if you haven't been keeping up with hollywood) is really cool. It lets us enrich our templating system with arbitrary monads. So, in this tutorial, we will be using Lucid instead of Blaze.

Lucid's internal data type is HtmlT m (), where m is some monad we can choose. To get you fresh with the syntax, look at the following code:

temp :: Monad m => HtmlT m () -> HtmlT m ()
temp content = doctypehtml_ $
  head_ [] $
    title_ [] "Foo Bar Land!"
  body_ [] $ do
    div_ [] $ do
      h1_ [] "Foo Bar Land!"
    div_ [] $ do
      p_ [] "woooooooo!"
      content

Sweet, huh? Honestly, it's simpler than Html. And statically checked! Now let's make a cool link:

temp :: Monad m => HtmlT m () -> HtmlT m ()
temp content = doctypehtml_ $
  head_ [] $
    title_ [] "Foo Bar Land!"
  body_ [] $ do
    div_ [] $ do
      h1_ [] "Foo Bar Land!"
    div_ [] $ do
      p_ [] "woooooooo!"
      a_ [href_ "http://google.com/"] "spooooky site! :o"
      content

Spooky, indeed. Now, let's analyze this for a moment - we've added a simple link into our template. Let's think about a URL trivially for a minute - there are three main parts you can categorize it by:

http://foo.com/foo/bar.html?some=data&that=stinks#ew
  • hostname: http://foo.com
  • location: /foo/bar.html
  • query: ?some=data&that=stinks#ew

When writing links in an Html template, it's crucial for the hostname to be interchangable - if I move my application to a new server or domain name, I would have a non-fun time renaming links. So there's one thing we should remember - keep hostnames modular for links.

The second major flaw with Html is importing - each foreign asset can be imported in two ways: inline, or hosted. Hosted assets also have a link - an href, so that's another place where discretionary linking (with a hostname) is important. But the important issue is that for some asset types, hosting the asset vs. inlining the asset may make for a different node type. Take Css for instance - to add inline styles, we use a <script> tag. But for external Css, we use a <link> tag. This makes migration lame as well. That's our second note - keep the use of resouces modular.


Urls

The urlpath package accomplishes the first goal for us. There are three monad transformers to help us choose how we should deploy - AbsoluteUrlT, RelativeUrlT, and GroundedUrlT. Using them is fairly simple; they are all MonadReaders, where the hostname will be the parameter when run. Running an AbsoluteUrlT prepends the hostname, RelativeUrlT leaves the location & query as-is, and GroundedUrlT makes sure there's a leading /, to show it's a root-oriented path.

There are a couple of functions and combinators you have to remember, but they're not too hard - first is the difference between a QueryString and a normal one -

foo :: QueryString
foo = "foo" <?> ("key", "value") <&> ("key", "value") <#> "hash"

bar :: QueryString
bar = "foo" <#> "hash"

Basically, a QueryString is a location with some form of parameter.

Using Urls

In order to generate a link for a template, we need to do it in somewhere in Lucid's monad parameter:

temp :: ( Url T.Text m
        , Monad m ) =>
        HtmlT m () -> HtmlT m ()
temp content = doctypehtml_ $
  head_ [] $ do
    title_ [] "Foo Bar Land!"
    
    css <- lift $ plainUrl "styles/main.css"
    link [rel_ css]
  body [] _ $ do
    div_ [] $ do
      h1_ [] "Foo Bar Land!"
    div_ [] $ do
      p_ [] "woooooooo!"
      a_ [href_ "http://google.com/"] "spooooky site! :o"
      
      crazy <- lift $ queryUrl $ "foo/bar" <?> ("key", "value") <#> "yo"
      a_ [href crazy] "crazy link, yo"
      content

Notice that we didn't take care of the Google link yet. Hold on.

The first thing we should notice is the Url constraint; this gives us a generic interface to the deployment monad we use - a common syntax. When we want to use the monad (whatever it is), we simply lift the url writing operation to the monad.

There are two Url writing operations - one for "plain" Urls (just a location as a string), and one for Urls with query parameters.

If you want to turn a Route from Yesod into a string one, use fromRoute . renderRoute.

Now we have the behavior of our Url rendering encoded and unified throughout our template. To render the template to text, we have to unpack our monad transformer. There's a generic function for this - runUrlReader:

rendered :: T.Text
rendered = (runUrlReader $ renderTextT $ temp content) "myawesomesite.com"

So we have a template with one overloaded hostname (that's the reason why we didn't make a link for google.com in this way). In order to render different urls, use the expandAbsoluteUrlWith function, and expect a MonadReader with the right input for your function:

data Env = Env { host1 :: T.Text
               , host2 :: T.Text }

temp :: ( MonadReader Env m
        , Monad m ) =>
        HtmlT m () -> HtmlT m ()
temp content = doctypehtml_ $
  head_ [] $ do
    title_ [] "Foo Bar Land!"
   
    css <- lift $ expandAbsoluteUrlWith host1 "styles/main.css"
    link [rel_ css]
  body [] _ $ do
    div_ [] $ do
      h1_ [] "Foo Bar Land!"
    div_ [] $ do
      p_ [] "woooooooo!"
      a_ [href_ "http://google.com/"] "spooooky site! :o"
    
      crazy <- lift $ expandAbsoluteUrlWith host2 $
                 "foo/bar" <?> ("key", "value") <#> "yo"
      a_ [href crazy] "crazy link, yo"
      content

In order to render this template, we can't use the overloaded rendering functions, we'll have to use the one for the particular monad you're in:

rendered :: T.Text
rendered = (runAbsoluteUrlT $ renderTextT $ temp content) (env :: Env)

The overloading is still very useful - with a simple type coercion, we can chage the behavior of all of the string-based routes in our template. And with the monad reader, we can change our app's hostname throughout the template at it's only generation point.


Markup

It's time to address the second major issue - differing Html nodes that reflect our particular choice of asset deployment. We have a lot of different parts to handle: if we have our assets stored in a subdirectory deployed locally, we will need our Url toolkit to generate links with our correct hostname. If we want to deploy assets from a CDN, we will have to take an absolute path as input. The same situation arises for inline assets - where the input is injected directly in the markup.

We're going to use a similar technique as urlpath to overload our Html. The markup package already has all the tools we need.

An Example

Sometimes it's best to just show how the machine works:

-- All the nodes adjacent to each other
styles :: Monad m => HtmlT m ()
styles = do
  renderMarkup inlineStyles
  renderMarkup hostedStyles
  renderMarkup localStyles


inlineStyles :: Monad m => InlineMarkupM (HtmlT m ())
inlineStyles = deploy Css "body{background:black;}"

hostedStyles :: Monad m => HostedMarkupM (HtmlT m ())
hostedStyles = mconcat
  [ deploy Css "https://code.jquery.com/jquery-2.1.4.min.js"
  , deploy Css "//cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css"
  ]

localStyles :: Monad m => HostedMarkupM (HtmlT (AbsoluteUrlT T.Text m) ())
localStyles = mconcat
  [ deploy Css ("styles/main.css" :: T.Text)
  , deploy Css $ ("styles/foo.css" :: T.Text) <?> ("key","cachebreak")
  ]

The styles term holds all of our Html content, and renderMarkup is just comonadic extraction - unwrapping our dummy monad. The real magic is in deploy - it's a multi-parameter typeclass function that does a lot of jumping around to find the right instance / behaviour to use - based on the type signature.

Here's the type of deploy:

deploy :: Deploy symbol input markup m =>
          symbol -> input -> m markup 

Very ad-hoc and has no inference power. To use deploy, we need a bit of preparation. First, we have a few singleton types representing abstract content:

data Css = Css
data JavaScript = JavaScript
data Image = Image

These will be used in place of symbol. We'll get back to the second variable, input. The third, markup, is the underlying type of templating engine we are using currently - our case is HtmlT m' (). The last is m - the dummy monad capturing the distinguished deployment behavior.

All the dummy monads are isomorphic to Identity - they are Functors, Applicatives, Monads, Comonads, and Monoids (if the contents are Monoids, too). That's why we can use mconcat above.

Now comes the tricky part - using the right instance for the input you provide. In the case of InlineMarkupM, it would need to be T.Text or LT.Text - lucius renders to lazy text, for instance. Or in HostedMarkupM, we're just using raw string links. In LocalMarkupM's case, we're expecting either T.Text (for plainUrl) or a QueryString (for queryUrl). Our website so far:

styles :: Monad m => HtmlT m ()
styles = do
  renderMarkup inlineStyles
  renderMarkup hostedStyles
  renderMarkup localStyles


inlineStyles :: Monad m => InlineMarkupM (HtmlT m ())
inlineStyles = deploy Css "body{background:black;}"

hostedStyles :: Monad m => HostedMarkupM (HtmlT m ())
hostedStyles = mconcat
  [ deploy Css "https://code.jquery.com/jquery-2.1.4.min.js"
  , deploy Css "//cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css"
  ]

localStyles :: Monad m => HostedMarkupM (HtmlT (AbsoluteUrlT T.Text m) ())
localStyles = mconcat
  [ deploy Css ("styles/main.css" :: T.Text)
  , deploy Css $ ("styles/foo.css" :: T.Text) <?> ("key","cachebreak")
  ]


temp :: ( Url T.Text m
        , Monad m ) =>
        HtmlT m () -> HtmlT m ()
temp content = doctypehtml_ $ do
  head_ [] $ do
    title_ [] "Foo Bar Land!"
    styles -- bam
  body [] _ $ do
    div_ [] $ do
      h1_ [] "Foo Bar Land!"
    div_ [] $ do
      p_ [] "woooooooo!"
      a_ [href_ "http://google.com/"] "spooooky site! :o"
      
      crazy <- lift $ queryUrl $ "foo/bar" <?> ("key", "value") <#> "yo"
      a_ [href crazy] "crazy link, yo"
      content

They will sing songs of our glorious website.

So this package solves our other problem for us - if we can provide enough type information to the outer m variable, markup variable, and input type, we can easily deploy different markup depending on our needs. Just change the monad, provide a correct input type, and you're all set.

Conclusion

Urlpath augments string-based Urls with an implicit hostname, which can be prepended with discrepency to your choice of deployment style. In markup, we have overloaded functions for generalizing the purpose of the Html, where we have a similar "change the type, change the deployment" pattern.

I hope you enjoyed this post! Please let me know if you run into any issues.