Mustache templates are popular logic-less templates that can be used in web development as well as in any other domain where interpolation of data into some sort of template is needed. “Logic-less templates” means that the templates themselves do not have any logic such as conditionals (although it's possible to choose how to represent empty collections for example) and thus it's much harder to shoot yourself in the foot when concerns of data processing and representation are separated.
Libraries supporting working with Mustache templates are available for many
languages, including Haskell (there are several of them, actually). In this
tutorial we will be using the
stache
package developed by
Stack Builders and show why this package is clear and easy to use.
The tutorial should be comprehensible for Haskell beginners and does not assume knowledge of any advanced Haskell-concepts.
Available libraries
Let's see which libraries are available to work with Mustache templates:
-
hastache
is the oldest (first released in 2011) and now deprecated library in this list. Those who used the library will probably agree that it doesn't have the clearest API where a lot of wrapping is necessary and dictionaries are represented as wrapped functions. Generics can be used to alleviate the situation, using yet another wrapper calledmkGenericContext
. -
mustache
is the official successor ofhastache
, but it again makes simple things complex using Aeson'sValue
(good) and at the same time introducing its ownValue
type (with conflicting names of constructors and naturally not so numerous instances). Source code is filled with Unicode symbols (using packages likebase-unicode-symbols
, etc.), so it's not easy to edit it if you want to contribute to the package. -
stache
is an alternative implementation that conforms to official Mustache spec and passes the official test suite. Its API consists of only 4 functions (3 to compile templates and one to render them using any instance ofToJSON
as source of data to interpolate), plus Template Haskell helpers to compile/validate templates at compile time. The parser is written with Megaparsec 5. The package usesData.Text.Lazy.Builder
under the hood and produces lazyText
.
The main motivation for developing stache
was the desire to expose a more
minimal API and use Aeson's instances directly for value interpolation, as
well as the desire to use Megaparsec instead of Parsec for parsing.
Initially we wanted to contribute to the existing mustache
library, but
then realized that the changes we wanted to implement were too cardinal and
we were better off writing our own package.
One feature that is not supported by stache
is lambdas. The feature is
marked as optional in the spec and can be emulated via processing of parsed
template representation. The decision to drop lambdas is intentional, for
the sake of simplicity and better integration with Aeson.
Compiling templates
Let's see how to compile templates because that's where you usually start. But first, let's take a look at types that will show up here and there in the rest of the tutorial:
-
PName
stands for “partial's name”. “Partials” are other templates that are inserted into the actual template you're rendering.PName
is a wrapper aroundText
and defined to make it harder to mix it up with other textual values. If you enable theOverloadedStrings
extension, you can writePName
s just as normalString
s. -
Node
is a piece of template. The whole template body is represented as[Node]
and that's all you need to know unless you plan to manipulate parsed representation of template, which is also easy, but not in the scope of this tutorial.
Three ways to compile a template
A Template
is actually a collection of templates (partials) with one of
them selected:
-- | Mustache template as name of “top-level” template and a collection of
-- all available templates (partials).
--
-- 'Template' is a 'Semigroup'. This means that you can combine 'Template's
-- (and their caches) using the ('<>') operator, the resulting 'Template'
-- will have the same currently selected template as the left one. Union of
-- caches is also left-biased.
data Template = Template
{ templateActual :: PName
-- ^ Name of currently “selected” template (top-level one).
, templateCache :: Map PName [Node]
-- ^ Collection of all templates that are available for interpolation
-- (as partials). The top-level one is also contained here and the
-- “focus” can be switched easily by modifying 'templateActual'.
} deriving (Eq, Ord, Show, Data, Typeable, Generic)
When working with a Template
, it's only possible to use partials that are
in templateCache
, thus the main difference between compiling functions is
where you get your template(s) and what you will have in templateCache
.
So, there are three different ways to get a Template
:
-
From lazy
Text
withcompileMustacheText
. The function takes aPName
and actual template source. The function returnsEither
parse error orTemplate
with cache consisting of only oneTemplate
. -
From a single
File
withcompileMustacheFile
. This one only takes the path to Mustache file. Note that the resulting template won't be able to use partials unless you combine several templates into one (see below). -
From a directory with
compileMustacheDir
. This function reads all templates (files ending with.mustache
extension) and puts them into the cache, selecting one of them as main (you specify which). The resultingTemplate
can use partials that were present in that directory.
So to have partials, we need to use compileMustacheDir
or combine
several templates into one using (<>)
method of Semigroup
type class.
Being an instance of the Semigroup
type class means that you can always
combine two Template
s and get their combination which will also be a
Template
. This is an incredible property, one of numerous examples how
Haskell keeps systems “flat”.
Let's start building practical stuff to see how things play together. As an example of a task that could involve Mustache templates, we will be generating a CUE sheet. First of all, we're going to need some imports:
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Main (main) where
import Data.Aeson
import Data.Semigroup ((<>))
import Data.Text (Text)
import GHC.Generics
import Text.Mustache
import qualified Text.Mustache.Compile.TH as TH
import qualified Data.Text.Lazy.IO as TIO
Then we will need to represent data somehow. For that, let's define the following data records:
data Release = Release
{ reGenre :: Text
, reDate :: Text
, reDiscId :: Text
, reComment :: Text
, rePerformer :: Text
, reTitle :: Text
, reFiles :: [File]
} deriving (Eq, Show, Generic)
instance ToJSON Release
data File = File
{ fiFileName :: Text
, fiIndex :: Int
, fiTitle :: Text
, fiPerformer :: Text
, fiIndex00 :: Text
} deriving (Eq, Show, Generic)
instance ToJSON File
Here we have defined two records with various fields, just like we would do
for other domains. We also have used
generics to quickly define
ToJSON
instance for the data types. You can read more about generics, but
the technique in a nutshell is simple: the compiler can derive Generic
instance for your datatype (with DeriveGeneric
extension enabled) which
describes “structure” of your data, and then you can use that information to
(again automatically) derive instance of ToJSON
type class.
Now we can experiment with template compilation. We will have two templates:
main.mustache
and file.mustache
(we could do with just one, but then I
wouldn't have a good example of how to work with partials).
The main.mustache
looks like this:
REM GENRE "{{& reGenre }}"
REM DATE "{{& reDate }}"
REM DISCID "{{& reDiscId }}"
REM COMMENT "{{& reComment }}"
PERFORMER "{{& rePerformer }}"
TITLE "{{& reTitle }}"
{{# reFiles }}
{{> file }}
{{/ reFiles }}
Here we have the typical layout of a CUE file header. The {{> file }}
part
inserts a partial which we can write this way (file.mustache
file):
FILE "{{& fiFileName }}" WAVE
TRACK {{& fiIndex }} AUDIO
TITLE "{{& fiTitle }}"
PERFORMER "{{& fiPerformer }}"
INDEX 00 {{& fiIndex00 }}
Given that the template files are stored in data/
directory, the following
code will produce a Template
with both of them stored in the cache (so we
can use one from another, like we intend to do):
main :: IO ()
main = do
mainTemplate <- compileMustacheFile "data/main.mustache" -- (1)
fileTemplate <- compileMustacheFile "data/file.mustache" -- (2)
let template = mainTemplate <> fileTemplate -- (3)
print template -- (4)
What's going on?
- We load
Template
fromdata/main.mustache
. - We load
Template
fromdata/file.mustache
. - We combine caches of the two templates and get another template which is
just like
main
but withfile
added to its cache. - We
print
the result because we don't know any better yet.
Another way to do the same is to use compileMustacheDir
:
main :: IO ()
main = do
template <- compileMustacheDir "main" "data/"
print template
Easy.
Template Haskell helpers
If you do not plan to change templates “on the fly” (without re-compiling your application), you may like Template Haskell (TH) helpers that compile Mustache templates at compile time and ensure that your templates are valid. If some template is not valid, your program just won't compile.
The TH helpers live in the Text.Mustache.Compile.TH
module and correspond
precisely to the three normal functions we already know. Let's play with a
template Haskell helper right in your editing environment.
main :: IO ()
main = do
let template = $(TH.compileMustacheText "main" "")
print template
Try to change the last argument of the TH.compileMustacheText
function and
if you have “on the fly” compilation and highlighting, you will be able to
observe error messages any time your input is not a valid Mustache template.
Invalid templates just won't compile!
For our purposes it's better yet to use the TH version of
compileMustacheDir
. Now if any template in the specified directory is
malformed, the program won't compile telling us about the error and where it
occurred.
main :: IO ()
main = do
let template = $(TH.compileMustacheDir "main" "data/")
print template
Note that the TH helpers only work with GHC 8 for now.
Rendering
There is just one function that you need to render your template —
renderMustache
. Its type signature looks like this:
renderMustache :: Template -> Value -> Text
First you feed it with a Template
(we just discovered how to get these),
then you need a source of values to interpolate. For that we need a Value
which comes from Aeson — a library for working with JSON in Haskell. Why to
do it this way? Well, Mustache templates originate from languages with duck
typing, so to feed values into a Mustache template, we need to categorize
them to understand how they should interact with the template we are
rendering. Aeson's Value
is exactly a type that is close to what you have
in Ruby (and of course in JavaScript), and since Aeson is a very popular
package, we already know how to convert pretty much any type to Value
.
Isn't that cool?
To interpolate something we need to have it first. Let's put together some data:
release :: Release
release = Release
{ reGenre = "Ambient"
, reDate = "1980"
, reDiscId = "380B7905"
, reComment = "ExactAudioCopy v0.95b4"
, rePerformer = "Laraaji"
, reTitle = "Ambient 3 Day Of Radiance"
, reFiles =
[ File
{ fiFileName = "01 - The Dance #1.wav"
, fiIndex = 1
, fiTitle = "The Dance #1"
, fiPerformer = "Laraaji"
, fiIndex00 = "00:00:00" }
, File
{ fiFileName = "02 - The Dance #2.wav"
, fiIndex = 2
, fiTitle = "The Dance #2"
, fiPerformer = "Laraaji"
, fiIndex00 = "09:06:10" }
]
}
Just use the renderMustache
function now with toJSON
(which transforms
instances of ToJSON
to Value
):
main :: IO ()
main = do
let template = $(TH.compileMustacheDir "main" "data/")
TIO.putStrLn $ renderMustache template (toJSON release)
As simple as that, if you run the program, you will see that our CUE file looks good:
REM GENRE "Ambient"
REM DATE "1980"
REM DISCID "380B7905"
REM COMMENT "ExactAudioCopy v0.95b4"
PERFORMER "Laraaji"
TITLE "Ambient 3 Day Of Radiance"
FILE "01 - The Dance #1.wav" WAVE
TRACK 1 AUDIO
TITLE "The Dance #1"
PERFORMER "Laraaji"
INDEX 00 00:00:00
FILE "02 - The Dance #2.wav" WAVE
TRACK 2 AUDIO
TITLE "The Dance #2"
PERFORMER "Laraaji"
INDEX 00 09:06:10
How does the magic work? What to do if you want to construct a Value
manually? Simply put, toJSON
does not do anything you can't do yourself.
Records are transformed into JSON Object
s, lists and vectors and turned
into Array
s, etc. Feeding the renderMustache
without toJSON
is just as
easy (consult
Aeson documentation for more
information), but the most common thing you will want to do is to create a
custom dictionary that will be your “context”. This is done with help of the
object
and (.=)
functions:
main :: IO ()
main = do
let template = $(TH.compileMustacheDir "main" "data/")
TIO.putStrLn $ renderMustache template $ object
[ "reGenre" .= "My Genre"
, "reDate" .= "2016"
, "reDiscId" .= "blahblah"
-- …
]
It just couldn't be easier.
Conclusion
stache
seems to do its job just fine so far. We migrated some code from
hastache
and were surprised just how simpler the code looked with
stache
. It's also nice to be able to check your templates at compile time,
like Shakespearean templates do. So in conclusion I have to say that I
wouldn't use anything but stache
to work with Mustache.