O'Reilly logo

Developing Web Applications with Haskell and Yesod by Michael Snoyman

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Calling Shakespeare

The question of course arises at some point: how do I actually use this stuff? There are three different ways to call out to Shakespeare from your Haskell code:

Quasiquotes

Quasiquotes allow you to embed arbitrary content within your Haskell, and for it to be converted into Haskell code at compile time.

External file

In this case, the template code is in a separate file which is referenced via Template Haskell.

Reload mode

Both of the above modes require a full recompile to see any changes. In reload mode, your template is kept in a separate file and referenced via Template Haskell. But at runtime, the external file is reparsed from scratch each time.

Note

Reload mode is not available for Hamlet, only for Cassius, Lucius, and Julius. There are too many sophisticated features in Hamlet that rely directly on the Haskell compiler and could not feasibly be reimplemented at runtime.

One of the first two approaches should be used in production. They both embed the entirety of the template in the final executable, simplifying deployment and increasing performance. The advantage of the quasiquoter is the simplicity: everything stays in a single file. For short templates, this can be a very good fit. However, in general, the external file approach is recommended because:

  • It follows nicely in the tradition of separate logic from presentation.

  • You can easily switch between external file and debug mode with some simple CPP macros, meaning you can keep rapid development and still achieve high performance in production.

Since these are special QuasiQuoters and Template Haskell functions, you need to be sure to enable the appropriate language extensions and use correct syntax. You can see a simple example of each in the examples.

Example 4-1. Quasiquoter

{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
{-# LANGUAGE QuasiQuotes #-}
import Text.Hamlet (HtmlUrl, hamlet)
import Data.Text (Text)
import Text.Blaze.Renderer.String (renderHtml)

data MyRoute = Home | Time | Stylesheet

render :: MyRoute -> [(Text, Text)] -> Text
render Home _ = "/home"
render Time _ = "/time"
render Stylesheet _ = "/style.css"

template :: Text -> HtmlUrl MyRoute
template title = [hamlet|
$doctype 5
<html>
    <head>
        <title>#{title}
        <link rel=stylesheet href=@{Stylesheet}>
    <body>
        <h1>#{title}
|]

main :: IO ()
main = putStrLn $ renderHtml $ template "My Title" render

Example 4-2. External file

{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE CPP #-} -- to control production versus debug
import Text.Lucius (CssUrl, luciusFile, luciusFileDebug, renderCss)
import Data.Text (Text)
import qualified Data.Text.Lazy.IO as TLIO

data MyRoute = Home | Time | Stylesheet

render :: MyRoute -> [(Text, Text)] -> Text
render Home _ = "/home"
render Time _ = "/time"
render Stylesheet _ = "/style.css"

template :: CssUrl MyRoute
#if PRODUCTION
template = $(luciusFile "template.lucius")
#else
template = $(luciusFileDebug "template.lucius")
#endif

main :: IO ()
main = TLIO.putStrLn $ renderCss $ template render
-- @template.lucius
foo { bar: baz }

The naming scheme for the functions is very consistent.

LanguageQuasiquoterExternal fileReload
Hamlet hamlet hamletFile N/A
Cassius cassius cassiusFile cassiusFileReload
Lucius lucius luciusFile luciusFileReload
Julius julius juliusFile juliusFileReload

Alternate Hamlet Types

So far, we’ve seen how to generate an HtmlUrl value from Hamlet, which is a piece of HTML with embedded type-safe URLs. There are currently three other values we can generate using Hamlet: plain HTML, HTML with URLs and internationalized messages, and widgets. That last one will be covered in the widgets chapter.

To generate plain HTML without any embedded URLs, we use “simplified Hamlet”. There are a few changes:

  • We use a different set of functions, prefixed with an “s”. So the quasiquoter is shamlet and the external file function is shamletFile. How we pronounce those is still up for debate.

  • No URL interpolation is allowed. Doing so will result in a compile-time error.

  • Embedding (the caret-interpolator) no longer allows arbitrary HtmlUrl values. The rule is that the embedded value must have the same type as the template itself, so in this case it must be Html. That means that for shamlet, embedding can be completely replaced with normal variable interpolation (with a hash).

Dealing with internationalization (i18n) in Hamlet is a bit complicated. Hamlet supports i18n via a message data type, very similar in concept and implementation to a type-safe URL. As a motivating example, let’s say we want to have an application that tells you hello and how many apples you have eaten. We could represent those messages with a data type.

data Msg = Hello | Apples Int

Next, we would want to be able to convert that into something human-readable, so we define some render functions:

renderEnglish :: Msg -> Text
renderEnglish Hello = "Hello"
renderEnglish (Apples 0) = "You did not buy any apples."
renderEnglish (Apples 1) = "You bought 1 apple."
renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]

Now we want to interpolate those Msg values directly in the template. For that, we use underscore interpolation.

$doctype 5
<html>
    <head>
        <title>i18n
    <body>
        <h1>_{Hello}
        <p>_{Apples count}

This kind of a template now needs some way to turn those values into HTML. So just like type-safe URLs, we pass in a render function. To represent this, we define a new type synonym:

type Render url = url -> [(Text, Text)] -> Text
type Translate msg = msg -> Html
type HtmlUrlI18n msg url = Translate msg -> Render url -> Html

At this point, you can pass renderEnglish, renderSpanish, or renderKlingon to this template, and it will generate nicely translated output (depending, of course, on the quality of your translators). The complete program is:

{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE OverloadedStrings #-}
import Data.Text (Text)
import qualified Data.Text as T
import Text.Hamlet (HtmlUrlI18n, ihamlet)
import Text.Blaze (toHtml)
import Text.Blaze.Renderer.String (renderHtml)

data MyRoute = Home | Time | Stylesheet

renderUrl :: MyRoute -> [(Text, Text)] -> Text
renderUrl Home _ = "/home"
renderUrl Time _ = "/time"
renderUrl Stylesheet _ = "/style.css"

data Msg = Hello | Apples Int

renderEnglish :: Msg -> Text
renderEnglish Hello = "Hello"
renderEnglish (Apples 0) = "You did not buy any apples."
renderEnglish (Apples 1) = "You bought 1 apple."
renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]

template :: Int -> HtmlUrlI18n Msg MyRoute
template count = [ihamlet|
$doctype 5
<html>
    <head>
        <title>i18n
    <body>
        <h1>_{Hello}
        <p>_{Apples count}
|]

main :: IO ()
main = putStrLn $ renderHtml
     $ (template 5) (toHtml . renderEnglish) renderUrl

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required