public
Last active

Comparing Snap's and Yesod's Template Languages Heist and Hamlet

  • Download Gist
gistfile1.md
Markdown

Comparing Snap's and Yesod's Template Languages Heist and Hamlet - and HSP

Currently, Snap and Yesod are the two most active web frameworks for Haskell. In this document, I compare the template languages used by those frameworks for generating HTML.

Both template languages let programmers write markup directly in a markup language rather than generating it from within Haskell. Compared with static HMTL, template languages support substitution to dynamically determine parts of the content of the generated page.

Heist and Hamlet support different constructs to generate content dynamically. They also differ regarding their syntax for static content.

Update

User stepcut251 pointed out in a comment on reddit that HSP, often used with the Happstack framework, is similar to Hamlet. I have added a discussion of HSP at the end of this document.

Syntax

Snap's template language Heist uses HTML syntax for static content. Even the constructs for dynamic content are written using XML tags. Therefore, Heist templates can be edited with support of many standard editors but inherit the verbosity of XML.

Yesod's template language Hamlet uses indentation to represent nesting such that closing tags are can be omitted. The omission of closing tags makes Hamlet files more concise and less error prone than corresponding HTML files. As a result, Hamlet files are not valid XML files which prevents editing them with standard editor support. The dynamic features of Hamlet use a custom syntax.

There are more fundamental differences between Heist and Hamlet than their syntax, however.

Substitution

Hamlet allows to embed arbitrary Haskell expressions to generate content. Content generation is handled by a type class and instances for various types are provided. Users can write things like

<p>#{name} is #{age} years old.

to reference Haskell variables name and age in a Hamlet template. If name = "Sebastian" and age = 32 this template will be rendered as follows.

<p>Sebastian is 32 years old.</p>

Yesod uses Template Haskell to call templates and all variables that are in scope at the call site can be substituted in the called template without explicitly passing them as arguments.

Heist allows to bind names within a template using the <bind> tag. Users can write things like

<bind tag="name">Sebastian</bind>
<bind tag="age">2<sup>5</sup></age>
<p><name /> is <age /> years old.</p>

to define and reference content bound to the tags <name /> and <age /> in a Heist template. This template will be rendered as follows.

<p>Sebastian is 2<sup>5</sup> years old.</p>

Arguments need to be passed explicitly when calling templates from another template or from Haskell. Bound tags can also be substituted inside attribute values using the following syntax.

<person name="${name}" age="${age}" />

Only text nodes are substituted inside attribute values such that this template will be rendered as follows if <name /> and <age /> are bound as before.

<person name="Sebastian" age="25" />

Hamlet templates can contain arbitrary Haskell expressions that may use all variables that are in scope when calling the template. In Heist, names need to be bound and passed explicitly.

Embedding Templates

In Hamlet, templates can be embedded in other templates using the ^ character. We can store the template shown above in a Haskell variable person and define another template to embed it.

<h2>Personal data
^{person}

If the variables name and age as well as person are in scope when calling this template it is rendered as follows.

<h2>Personal data</h2>
<p>Sebastian is 32 years old</p> 

In Heist we can embed templates using the <apply> tag. If we store the following template in a file person.tpl

<p><name /> is <age /> years old.</p>

then we can embed it in another template as follows.

<h2>Personal data</h2>
<apply template="person">
    <bind tag="name">Sebastian</bind>
    <bind tag="age">2<sup>5</sup></bind>
</apply>

This template will be rendered as

<h2>Personal data</h2>
<p>Sebastian is 2<sup>5</sup> years old</p> 

Heist binds a special tag <content /> when applying templates. It contains the contents of the <apply> tag without the <bind> tags and can be used in the applied template. This is often used to define a default layout without repeating it in every page:

<apply template="layout">
    <h1>Mostly Lost</h1>
    <p>Loren Ipsum had been lost in the woods all morning. 
       The poor girl didn't know where she was or where she was going.</p>
</apply>

This template will render to a complete HTML page if the file layout.tpl contains such a page using the <content /> tag in it's body.

While Heist templates take explicit arguments, Hamlet templates can implicitly use all variables that are in scope when calling a template. The explicit argument passing of Heist makes it possible to embed another template twice with different arguments.

Type-safe URLs

Hamlet provides another form of substitution to generate URLs. Users can write something like

<a href="@{HomeR}">Go back home

to define a link to a URL specified using a Haskell constructor HomeR. Yesod automatically generates such constructors from routing tables and checks their use in Hamlet templates at compile time. Using these constructors prevents errors when defining links. The constructors representing URLs may also have arguments if the corresponding route is defined using a wildcard, such as

/person/#Int PersonR

This route gives rise to the definition of a construtor PersonR with an argument of type Int that can be used to construct a link dynamically based on a run-time identifier:

<a href="@{PersonR ident}">Link to person with identifier #{ident}

Heist does not allow to specify dynamically generated links in templates directly. But they can be created using splices.

Splices

Rather than binding tags to fixed content, Heist also allows them to be bound to Haskell functions generating such content dynamically. Splices can be seen as a restricted form of the substitution of arbitrary Haskell values in Hamlet.

In Heist, we can define the following Haskell function to generate a link such as the one shown above.

personLink ident =
    return [Element "a" [("href", mappend "/person/" identText)] $
               TextNode (mappend "Link to person with identifier " identText)]
  where
    identText = pack $ show ident

Note that we construct the URL to the link manually which is error prone and may be difficult to maintain when routes change.

If we bind the result of the above function for a specific value for ident (say 42) to a tag then this tag will be replaced with the following link when rendering a template.

<a href="/person/42">Link to person with identifier 42</a>

Heist requires all dynamic content to be generated using splices. As a consequence, dynamically generated markup needs to be specified in Haskell and cannot be written in the template file like with Hamlet.

Another use case for splices are loops to iterate over a collection of items to be listed on a page or conditional content, for example based on whether users are currently logged in. Hamlet has special syntactical constructs for loops and conditionals and allows to generate content dynamically directly in the template.

Combining Yesod with Heist

Hamlet's advantages are concision and increased safety when generating links. Heist's advantages are a widely supported syntax and a strict separation between static and dynamic content.

While it is impossible to get both concise and widely supported syntax, it is in fact possible to use Heist with Yesod and combine Heist's strict separation of static and dynamic content with URLs based on Yesod's routing. Users in favor of Heist's advantages can define a splice that uses Yesod's URL rendering function to automatically generate links based on Yesod's datatypes for URLs, such that a link like

<a href="PersonR 42">Link to person with identifier 42</a>

will be rendered as

<a href="/person/42">Link to person with identifier 42</a>

automatically. However, errors in the values of the href attribute will still be detected only at run time using this approach because Heist templates are loaded dynamically, not statically like Hamlet templates. Using Heist's onLoad hook, it is possible to throw the error when loading the template rather than when rendering it, which is nearly as useful as a static guarantee. Thanks, mightybyte, for pointing this out in a comment on reddit. However, a check on load is not possible for URLs that depend on values passed to the template as argument. For example, Hamlet can check if the link

<a href="@{PersonR ident}">Person with identifier #{ident}

corresponds to a well-formed route, while Heist cannot check the same for

<ca href="PersonR ${ident}">Person with identifier <ident /></ca>

if ident is a template argument, because it does not know the type of ident.

Like Yesod, the web-routes package provides safe generation of links using custom datatypes for URLs. However, it also doesn't help with checking URLs in Heist templates before they are actually rendered.

Comparing Hamlet and HSP

HSP comes with a preprocessor HSX that allows users to write XML directly in Haskell source files. It also comes with a way to substitute Haskell expressions into the generated documents and provides similar compile-type guarantees as Hamlet.

For example, we can write a template for people as follows using HSP.

person name age = <p><% name %> is <% age %> years old.</p>

Attribute values are directly interpreted as Haskell values so type-safe routing can be written without any escape syntax.

home = <a href=HomeR>Go back home</a>

This template can be used after defining a type-class instance that tells HSX how to render the URL datatype.

Other templates can be embedded by substituting them using <% ... %> as well. Yesod uses different forms of substitution in different contexts to be more restrictive. For example, it is a type error to splice something that is no URL using the @{...} syntax and also to splice URLs that belong to a sub site without proper wrapping. Thanks, Michael Snoyman, for pointing that out.

In Hamlet templates, Haskell variables from the call site are in scope automatically, even if the template is stored in a separate file. For example, we can create a file person.hamlet with the following content

<p>#{name} is #{age} years old

and call it using $(hamletFile "person.hamlet") from within Haskell where name and age are in scope. If we want to store an HSX template in a separate file, we need to pass arguments explicitly. For example, we can define a file person.hs with the following content

person name age = <p><% name %> is <% age %> years old.</p>

and then call person name age from a different Haskell file.

Finally, Hamlet provides special purpose syntax for conditionals and loops. For example, we can render a list of people using the $forall construct in Hamlet.

<ul>
    $forall (name,age) <- people
        <li>#{name} is #{age} years old.

In HSX, substituted lists of expressions are concatenated, such that we can achieve the same result with do notation in the list monad using HSX:

renderPeople people =
    <ul>
        <% do
            (name,age) <- people
            return <li><% name %> is <% age %> years old.</li> %>
    </ul>

Both templates will render as follows if people is bound to the list [("Sebastian,2^5)].

<ul>
    <li>Sebastian is 32 years old.</li>
</ul>

HSP in Happstack is based on an XML datatype using Strings internally. Hamlet is based on blaze-html which promises much better performance. Conceptually, HSX can use blaze-hmtl too. HSX relies on type classes and corresponding instances could be defined for the datatypes in the xmlhtml package.

Such instances could not only speed up HSP but also make it trivial to use HSX together with Yesod.

In my testing, blaze-html could handle about twice the requests per second compared to HSP, but it should be noted here that HSP is way more powerful. In the future it will use text. I think in a dynamic web application, the power HSP provides is more important than the raw performance of blaze. For example it's really painful to do type safe routes and i18n with blaze. You can use hamlet on top of blaze for those specific features, but with HSP you get to invent any features of that sort you need.

BTW I'd use a list comprehension in your renderPeople example:

renderPeople people = <ul><% [ <li><% name %> is <% age %> years old.</li> | (name,age) <- people ] %></ul>

Seems like the recommended way to do i18n in Happstack is to use Yesod's library ;)

P.S. I also like your list comprehension version better than mine using do notation. I leave it as is to keep the vague resemblance with Hamlet's $forall construct.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.