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 MonadReader
s, 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, usefromRoute . 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.