Serenade in Haskell
One of the things that I think is great about Haskell is the way that you can use the language to design new syntax. After reading a quick introduction to Serenade.js, one of the features that caught my eye was their templating system. It has a fairly elegant interface that provides a concise language for generating HTML, though it seems like a heavyweight solution, as it requires implementing a parser.
Upon closer inspection the examples provided seemed to be presenting a few key combinators for constructing html. After a bit of scaffolding, I’ll demonstrate my approach to replicating their examples in Haskell. Just to be clear, this isn’t meant as a replacement, or a justification for templating in your source language, but rather just an exploration of a Haskell embedding.
Preliminaries
{-# LANGUAGE OverloadedStrings #-}
module Serenade where
import Text.PrettyPrint.HughesPJ
If you don’t already know the pretty package on hackage, it’s worth learning. Many of the unpleasant tasks of generating strings can be boiled down to elegant uses of the combinators it provides.
The type that the pretty package defines for pretty-printed documents is the
Doc
type. As I am going to be rendering out HTML as text in my Serenade
type, it’s natural to make it a synonym of the Doc
type.
type Serenade = Doc
I’ve simplified attributes to key/value pairs, which works well for my small example. Their pretty printing just involves printing the name as text, followed by an ‘=’ character, then the double quoted value.
type Attr = (String,String)
attr :: Attr -> Serenade
attr (n,v) = text n <> char '=' <> doubleQuotes (text v)
attrs :: [Attr] -> Serenade
attrs = hsep . map attr
Tags are implemented as functions from their attributes and a child document, to
a new document. The tag
primitive is a function that takes a boolean value
that specifies if its content should be nested, or on the same line, and the
name of the tag as a String
. When the resulting Tag
is used, it prints out
open/close pairs of tags with their attributes, optionally placing the child
content of the tag on a new line, and indenting it by two spaces when it is.
There is currently no notion of a empty tag, such as <br />
, though it would
be easy to generate that tag when the child argument was empty.
type Tag = [Attr] -> Serenade -> Serenade
tag :: Bool -> String -> Tag
tag nl name as child =
char '<' <> text name <+> attrs as <> char '>'
^^ nest 2 child
^^ text "</" <> text name <> char '>'
where
infixr 0 ^^
(^^) | nl = ($$)
| otherwise = (<>)
Combinators
One of the things that struck me about the Serenade templates was their simplicity: you don’t write out the open/close tag pairs, relying on layout to denote blocks. Being a Haskell programmer, this just seemed right. Let’s start by adding a few tag definitions so that the rest of the examples have some motivation:
ul, li, h1 :: Tag
ul = tag True "ul"
li = tag False "li"
h1 = tag False "h1"
Attributes
The first example that caught my eye was the difference between using a tag with
a list of attributes, and using a tag with a specific attribute: id
. There
are two flavors of syntax in Serenade to support this functionality, the most
general form first:
ul[id="x"] ...
ul#"x" ...
In the first example, you are just applying the ul
tag to a list list of
attributes, whereas in the second example, you’re using some special syntax to
set only the id
attribute. I’ve chosen to implement this as a function that
takes something a little more general than the Tag
type specified above, as
there’s no reason to rule out other uses of this pattern.
(#) :: ([Attr] -> a) -> String -> a
k # i = k [("id", i)]
attr_example1 = ul [("id", "x")]
attr_example2 = ul # "x"
If you recall the definition of the Tag
synonym, you can substitute
Serenade -> Serenade
for a
in the type of (#)
, and see that it can quite
easily be used as something of type Tag -> String -> Serenade
.
Variables
In Serenade, variables are used by prefixing an identifier with a @
character.
For example, if I have the name
variable in scope, and would like to use it in
the body of an li
tag, I can do this with the following snippet:
li Hello, my name is @name
The way that I might approach this in Haskell is to view @
as something that
joins a piece of text with the value of a variable. Assuming that all
variables in Serenade contain strings, this can be viewed as a combinator that
takes some Serenade
thing on the left, and a variable that contains a String
on the right:
(@@) :: Serenade -> String -> Serenade
l @@ r = l <> text r
Thus, the original example that uses the li
tag can be turned into a function
that expects to have its name
value provided.
li_example :: String -> Serenade
li_example name = li [] ("Hello, my name is " @@name)
Modulo the extra @
symbol to avoid the built-in use of @
in Haskell, this
conveys the intent of the original serenade template.
Collections
Serenade provides a way to, given a collection of key/value structures, map a serenade template over each structure.
ul#comments
- collection @comments
li @title
As a Haskell programmer, this seems like the map
function, but with a little
bit of extra information about how to put together the results. The vcat
function from the pretty library covers how we’d like to join together the
documents generated from each element of the list, joining them together on
separate lines. As a result, the Haskell implementation of the collection
function in Serenade should end up being just as powerful: anything that you
can make into a Serenade
thing, you can lift over lists.
collection :: [a] -> (a -> Serenade) -> Serenade
collection as k = vcat (map k as)
Now, the example above can be encoded using the new collection
combinator as
such, using the text
function from the pretty library to turn the title string
into a Serenade document.
col_example comments = ul # "comments"
$ collection comments
$ \title -> li [] (text title)
Ignoring the lambda, and the introduction of the comments as an argument to the example, this looks quite similar to the example in the Serenade templating language. Focusing on the lambda, the programmer now has control over the naming of the fields in the collection; in Serenade, you would be coupled to the field names defined by the code calling the template, whereas in the Haskell version, the programmer gets to use normal conventions introduce names that are convenient to them.
The neat thing about the collection
combinator defined in Haskell is that it
only cares about the fact that you give it a list of things, and a way to turn
an individual thing into a Serenade
thing. If you wanted to take a list of
lists that contained titles, and flatten them into a single list of titles, you
could modify col_example
to look like this:
col_of cols = ul # "cols"
$ collection cols -- outer collection
$ \ col -> collection col -- inner collection
$ \ title -> li [] (text title)
Now each item of the outer collection is passed again to another use of the
collection
combinator, emitting one li
tag for each inner title. When given
the list [ ["a"], ["b", "c"] ]
, the col_of
function will produce the HTML:
<ul id="cols">
<li>a</li>
<li>b</li>
<li>c</li>
</ul>
Conclusion
It’s amazing what you can do with just functions in Haskell. Careful attention by the language designers to things like name scope mean that you no longer need to rely on the names that someone else has chosen, you can choose your own names and expect them to remain stable. Templates written in the Haskell Serenade approximation outlined above also benefit from Haskell’s type system, in that we’re not bound to viewing everything as a hash of values: we can write functions that traverse structures in a more meaningful way, without falling back on hash tables.