I felt that compiled Heist warranted a new tutorial, as the information available seems pretty fragmented. At least it's not yet another monad tutorial though you'll need to know about monads. I'll be trying to convey a way to think about Heist and provide context to the functions provided in compiled Heist.
Heist is an HTML or XML templating library. It comes as default with Snap Haskell web framework and I'll cover using Heist in that context.
I won't be talking about interpreted Heist and this tutorial assumes no familiarity with it.
On a high level, Heist allows writing templates using plain HTML and XML and mixing in splices in there. Splices are mapped to Haskell code and the program logic resides on Haskell side. A template may look like this:
<p>
Lorem ipsum <a href="foo.html">dolor sit</a> amet...
<div>
<h:repeat times="2">
<span><h:hello/></span>
</h:repeat>
</div>
</p>
This template demonstrates two splices, repeat
and hello
. Note
the h:
prefix in the tags. This is the namespace of the tags.
The default action to do with anything that isn't recognized as a
splice is to pass them to the generated document. Namespaces can be
used to identify any splices, which allows the parser to recognize any
undefined splices and to make error messages about them.
A number of example programs come along with this tutorial. To run
the first example, build the included cabal project and run tutorial example1
or use cabal repl
and call example1
. I encourage you to
look up the code and to make changes to it to test it out. I have
chosen to not use Literate Haskell for this tutorial to show my
examples as close to code you'd write in a real program as possible.
This file has the main outline of the tutorial but various details are
stepped through in the interspresed examples.
Functions used in example1
: runChildren
, yieldPureText
.
Compiled Heist separates templating code into two different realms,
load time and runtime. The separation is enforced by using Splice
and RuntimeSplice
types. Splice
is a computation that returns a
list of chunks (more on that later). RuntimeSplice
is a monad for
runtime splice execution. A monad can be thought of in many ways but
thinking of them as things that facilitate actions, or computations,
may be helpful in this context. Heist comes with a number of
functions for managing the interaction between these two.
This might remind you of Template Haskell, though Heist doesn't make
use of that for this. What it does is to compile (as in, transform,
not as in "invoke GHC") all the templates at load time to actions of
RuntimeSplice
type and chunks of static content. It's not type
safety per se, but it still allows for writing for checks for
templates that trigger at program startup time instead of having them
as run time errors.
Another point is that moving logic to load time makes compiled Heist perform faster than interpreted Heist, which only has runtime to contend with. But this is, in my opinion, a secondary issue.
A key to understanding any Haskell code is to reason about the types it uses. To a degree, seeing the type declarations is already a big part of the documentation.
type Splice n = HeistT n IO (DList (Chunk n))
Splice n
is one of the two main actors in using compiled Heist. As
you can see, it is an alias for a monad transformer and we'll walk
through its parts. It represents a computation that returns a DList (Chunk n)
. A DList
is a difference list and it suffices to think
of it as a list. Chunk n
is an opaque type representing either a
static part of a web site taken from a template or generated by a
splice, or a runtime action. It's not necessary to concern with the
how of these mechanics, but it's useful to keep in mind that when you
have a Splice n
and use bind on values, you'll get something that
can be mappend
'ed together and passed to a return. Conversely, a
number of functions return a DList (Chunk n)
so it helps to remind
yourself of that all it takes to go from there to a Splice n
is a
return
.
Another thing to take from this is that when you see Splice n
is
that the n
in it stands for the runtime type, not a "contained"
type. Splice n
is not the monad in this. You may have something of
type Splice n
in your code and do val <- something
, but that is a
bind of HeistT n IO (DList (Chunk n))
, not of Splice n
. You'll
always get something of type DList (Chunk n)
out of that.
IO
in the type is used for compiling templates. Splices can load
other templates, which requires IO
for it and you'll have access to
liftIO
in your splice code.
n
is your runtime monad. If you are using Heist with Snap then this
will most likely be like Handler App App
(or even more likely a type
alias via type AppHandler = Handler App App
), but it could be
anything else, like State m
. The Chunk
in the return type has
access to this type.
GHC will give you a HeistT
in any type error messages instead of a
Splice
so you should keep in mind that it's about the latter.
newtype RuntimeSplice m a
This is the counterpart for Splice
. Again, m
is your runtime
monad. Where compiled Heist uses a RuntimeSplice
, you'll often see
the return type to be either a
or ()
or Builder
. Builder
is a
type from blaze-html
library, representing a stream of bytes. It suffices to think of it
as the ultimate output of a runtime action. It is a monoid and as
such they can be concatenated. Functions using the unit type are,
obviously, used for things that are used for their side effects in the
runtime monad.
Beyond ()
and Builder
, compiled Heist has a number of functions of
the form (a -> RuntimeSplice n b) -> (RuntimeSplice n b -> Splice n)
. These offer higher level control patterns which make use of user
supplied functions. We'll return to these later on.
type AttrSplice m = Text -> RuntimeSplice m [(Text, Text)]
This is the third Heist type seen in the context of a
RuntimeSplice
. Attribute splices can be used to conditionally set
attribute values or omit them from the output.
type Splices s = MapSyntax Text s
A map of splices. The key is the splice name and the value is, if
used at a top level, Splice n
, or RuntimeSplice n a -> Splice a
if
used with withSplices
. It's often useful to define splices that are
valid only if used within another splice and they make use of runtime
data computed at an upper level.
A few words about semantics.
As described above, Splice n
is a data type representing something
that becomes a byte string output, with or without using runtime data
for it. Or a list of Chunks, if you will.
On another level, splice is something bound to a text string, more
specifically to a tag like <h:splice/>
or <div id="${h:splice}"/>
.
A splice, as a tag, has a position in a template file and it may or
may not have child elements. You can use getParamNode
in your
splice function to access that and it's what functions like
runChildren
use. Processing a splice will trigger other splices,
which may use the same runtime data or some computation based on it or
something from a wholly different source.
Speaking of plural splices may often refer to the map of splices in use. That's how the Heist documentation ends up with phrases like
Runs a splice, but first binds splices given by splice functions that need some runtime data.
That is, run a collection of Chunks, first manipulating the map of
recognized splices by using functions that take some runtime data and
return a Splice n
.
Let's take a slight digression to see how Heist is initialized in Snap.
app :: SnapletInit App App
app ex = makeSnaplet "tutorial" "Heist tutorial" Nothing $ do
h <- nestSnaplet "" heist $ heistInit "tutorial"
addConfig h $ mempty .~ scCompiledSplices tutorialSplices
return $ App h
tutorialSplices :: Splices (Splice AppHandler)
tutorialSplices = do
"foo" ## return $ yieldPureText "hello world"
"bar" ## barImpl
barImpl :: Splice AppHandler
barImpl = undefined -- to be continued
Heist works as a Snap snaplet and it is initialized in the main
application snaplet initializer. Splices are defined with
addConfig
. mempty .~ scCompiledSplices tutorialSplices
uses lens
to set compiled splices to an empty splice config. Splice config is a
monoid to allow this kind of an operation. If you haven't seen lens
before, I would suggest to not worry about their use overly much. The
theory behind them gets difficult fast but they are straightforward to
use in practice.
tutorialSplices
defines the splices, in this example two of them.
The ##
operator is from
map-syntax package.
It's a useful shorthand for an operation that's often used when
defining splices.
Splice definitions follow a general pattern where you have a map of
Text
and Splice n
pairs on the top level. To actually do some
work in your splices and not just display static content, you'll need
some RuntimeSplice m a
actions. A splice can recursively use other
splices and you can transform the runtime data to other forms more
suitable for them.
Please see example2
to see some of these operations in action.
Functions used in example2
: pureSplice
, textSplice
,
manyWithSplices
, runChildren
, yieldRuntimeText
, deferMap
,
deferMany
.
We'll have a further look at these functions later on.
Let's have a closer look at some of the functions which concern
Builder
values. See example1
for a program demonstrating these
functions' use.
textSplice :: (a -> Text) -> a -> Builder
xmlNodeSplice :: (a -> [Node]) -> a -> Builder
htmlNodeSplice :: (a -> [Node]) -> a -> Builder
pureSplice :: Monad n => (a -> Builder) -> RuntimeSplice n a -> Splice n
yieldPure :: Builder -> DList (Chunk n)
Recall that Builder
, as a type, concerns runtime output. The
three1 fooSplice functions can see use
inside a function like yieldRuntime
or with pureSplice
or
yieldPure
. They all take a function that turns an input a
into a
value type and something of type a
. For simplicity's sake, I'll use
textSplice
as the example for this section.
pureSplice
is typically used along with textSplice
to make a
function that turns runtime values to Text
and then to splices.
pureTextSplice :: Monad n => (a -> Text) -> RuntimeSplice n a -> Splice n
pureTextSplice = pureSplice . textSplice
The function composition used, while succinct, may be a bit too terse for demonstration purposes. Let's make another one, though this is a bit contrived.
combinedSplice :: Monad n => RuntimeSplice n (Text, [Node]) -> Splice n
combinedSplice = pureSplice (\x -> textSplice fst x <> nodeSplice snd x)
Similarly, yieldPure
generates splices based on data known at load
time.
sndToChunk :: (a, Text) -> Splice n
sndToChunk = return . yieldPure . textSplice snd
The most basic way of creating new splices is to take the current splice's children or fetching some other template from a file.
runChildren :: Monad n => Splice n
callTemplate :: Monad n => ByteString -> Splice n
Beyond these, a Node
or a list of them can be used to create a
splice.
runNode :: Monad n => Node -> Splice n
runNodeList :: Monad n => [Node] -> Splice n
To control in more depth what goes into creating those chunks, the yield family of functions can be used.
yieldPure :: Builder -> DList (Chunk n)
yieldRuntime :: RuntimeSplice n Builder -> DList (Chunk n)
yieldRuntimeEffect :: Monad n => RuntimeSplice n () -> DList (Chunk n)
yieldPureText :: Text -> DList (Chunk n)
yieldRuntimeText :: Monad n => RuntimeSplice n Text -> DList (Chunk n)
The typical way to use these is to immediately combine it with a
return to give you a Splice n
though the yield functions don't do it
to allow operating on pure DList (Chunk n)
values.
codeGen :: Monad n => DList (Chunk n) -> RuntimeSplice n Builder
Here we have the inverse function of yieldRuntime
. It can be used
along with yieldRuntime
to control how a splice is rendered at
runtime. For example:
conditionalRender :: Monad n => RuntimeSplice n Bool -> Splice n
conditionalRender runtimeAction = do
childContent <- runChildren
return $ yieldRuntime $ do
conditional <- runtimeAction
return if conditional then codeGen childContent else mempty
Note how calling runChildren
in this snippet makes the compiled
splice loader process and apply any splices inside the child elements
at load time and it's the runtime data that determines whether any of
them have any effect.
Another point worth emphasizing about this code sample is that
yieldRuntime
is a function that takes a RuntimeAction n Bool
value
as it's argument. That's everything after the second do
. It's a
monadic computation in a wholly different monad than the HeistT
at
the upper level. There's a duality going on in here, having a
RuntimeAction n Bool
value both as a function argument and as a
computation. This is Haskell and functional programming in action,
allowing making new control structures on the go.
The yieldRuntimeEffect
function is useful for doing side effects.
We'll cover more of its uses when we talk about promises, but let's
make a simple if contrived example.
greeterIOSplice :: MonadIO n => Splice n
greeterIOSplice = do
content <- runChildren
return $ content <> (yieldRuntimeEffect $ liftIO $ print "hello")
withSplices :: Monad n => Splice n -> Splices (RuntimeSplice n a -> Splice n) -> RuntimeSplice n a -> Splice n
manyWithSplices :: (Foldable f, Monad n) => Splice n -> Splices (RuntimeSplice n a -> Splice n) -> RuntimeSplice n (f a) -> Splice n
manyWith :: (Foldable f, Monad n) => Splice n -> Splices (RuntimeSplice n a -> Splice n) -> Splices (RuntimeSplice n a -> AttrSplice n) -> RuntimeSplice n (f a) -> Splice n
withLocalSplices :: Splices (Splice n) -> Splices (AttrSplice n) -> HeistT n IO a -> HeistT n IO a
We already encountered manyWithSplices
in example2
. The
withSplice
function is the same, but it concerns a singular runtime
value. These two functions take a splice, a map of functions that
take a runtime data and return a splice and the runtime data to use.
A point worth highlighting about these functions: The previously
defined splices are still available for use in the new splices you
define. The upper level splices are not affected with whatever
manipulation of the runtime data you've done with manyWithSplices
or
the yield family of functions. If you fall into thinking of things
imperatively, you may be concerned about the upper level splices
getting a different runtime data when used in splices across a
withSplices
. But that's not what's going on with it.
The manyWith
function is a version of manyWithSplices
which also
takes attribute splices. We'll return to those later on.
The withLocalSplices
function is a bit lower level version of the
same, which doesn't concern itself with the runtime data. Recall that
HeistT n IO a
is a generalisation of the usual Splice n
, that is,
HeistT n IO (DList (Chunk n))
that you usually see.
These functions work often well together. This function was defined previously but let's repeat it:
pureTextSplice :: Monad n => (a -> Text) -> RuntimeSplice n a -> Splice n
pureTextSplice = pureSplice . textSplice
If your runtime data was plain Text
, you could use:
import Data.Text (Text)
import qualified Data.Text as T
simpleTextSplices :: Splices (RuntimeSplice m Text -> Splice n)
simpleTextSplices = do
"printdata" ## pureSplice . textSplice
"appended" ## pureSplice . textSplice $ T.append (T.pack "123")
Note how $
is used on the right hand side of the pureSplice . textSplice
to apply a function which takes a Text
value.
Another function from Data.Map.Syntax
which could see use in a case
where the same function is used to make a splice with the same
function is mapV
:
simpleTextSplices' :: Splices (RuntimeSplice m Text -> Splice n)
simpleTextSplices' = mapV (pureSplice . textSplice) $ do
"printdata" ## id
"appended" ## T.append (T.pack "123")
We already saw this group of functions in action in example2
.
defer :: Monad n => (RuntimeSplice n a -> Splice n) -> RuntimeSplice n a -> Splice n
deferMany :: (Foldable f, Monad n) => (RuntimeSplice n a -> Splice n) -> RuntimeSplice n (f a) -> Splice n
deferMap :: Monad n => (a -> RuntimeSplice n b) -> (RuntimeSplice n b -> Splice n) -> RuntimeSplice n a -> Splice n
mayDeferMap :: Monad n => (a -> RuntimeSplice n (Maybe b)) -> (RuntimeSplice n b -> Splice n) -> RuntimeSplice n a -> Splice n
bindLater :: Monad n => (a -> RuntimeSplice n Builder) -> RuntimeSplice n a -> Splice n
defer
should look familiar: It is a specialization of function
application, $
. It takes the value from the runtime computation in
the second argument and stores it into a promise which the function
in the first argument uses. Think of it as a kind of a barrier.
Anything on the other side of it gets computed again every time it
gets used, but having a defer in between will insulate you from that
effect. You'll want this when your runtime computation involves IO
like database access.
deferMany
is the "for each" of the runtime manipulations. It takes
a runtime value and turns it into singular values and passes them to a
function returning a new splice.
deferMap
is a combination of fmap
and defer
. Typically you'll
want to manipulate the runtime value in some manner prior to storing
it with defer
and this function may turn out to be a convenience.
mayDeferMap
is the same but it allows short circuiting the splice
generated by the provided function. Note that there is no mayDefer
,
but thanks to the Foldable instance of Maybe, just use deferMany
.
These functions offer ways to manipulate the runtime value and to pass
it to other splices but you should keep in mind that the runtime
value, as a monad, can be altered just as usual with fmap
, =<<
and
so on. You may get repeated computations but with lightweight pure
computations that hardly is an issue.
simpleRuntimeOps :: RuntimeSplice m (Int, Text) -> Splice n
simpleRuntimeOps n = flip defer n $ \n' -> do
c1 <- withSplices (callTemplate "_foo") fooSplices $ fst <$> n'
c2 <- withSplices (callTemplate "_bar") barSplices $ snd <$> n'
return $ c1 <> c2
fooSplices :: Splices (RuntimeSplice m Int -> Splice n)
barSplices :: Splices (RuntimeSplice m Text -> Splice n)
The defer
call in the above example insulates the local runtime
computation from whatever extraneous computation it may have prior to
the simpleRuntimeOps
call. There's no need to use deferMap
with
the withSplices
calls as using fmap
with fst
and snd
incurs no
further computations.
bindLater
uses a function that generates output from a runtime value
on a runtime value. It's a light wrapper on yieldRuntime
. If the
first thing you do in a yieldRuntime
action is to do val <- someRuntimeValue
then look into using bindLater
.
A rule of thumb about these functions naming: they "defer" and do
things "later" in the sense that they are all executed in Splice n
but they just set up things for the runtime actions, hence their
effect isn't immediate.
Promises are the low level mechanism used to convey or store a transformed value at run time. The defer family of functions all use promises to plumb through altered runtime values. Promises exist at splice level and can be passed around and act as placeholders, but all the operations for setting or getting their values belong to the runtime world.
import Heist.Compiled.LowLevel
newEmptyPromise :: HeistT n IO (Promise a)
getPromise :: Monad n => Promise a -> RuntimeSplice n a
putPromise :: Monad n => Promise a -> a -> RuntimeSplice n ()
adjustPromise :: Monad n => Promise a -> (a -> a) -> RuntimeSplice n ()
newEmptyPromise
creates a new promise. Note that it's done at
splice level.
getPromise
and putPromise
get and set a promise's value,
respectively. adjustPromise
manipulates it.
An example of using promises directly:
greeterPromisesSplice :: Splice n
greeterPromisesSplice = do
p <- newEmptyPromise
let helloAction = yieldRuntimeEffect $ putPromise p "hello"
output <- bindLater (\hello' -> do
let world = fromString " world"
return $ hello' <> world) (getPromise p)
return helloAction <> output
If you find that you are using promises in your code, it may be worth it to take a look at it and think if there could be some generic function you could abstract your use inside.
You may want to look at the example3
code along with this section.
Attribute substitution can be done with the ${}
syntax.
<html>
<body>
<div id="${h:foo}">asdf</div>
</body>
</html>
attrExampleSplice :: Splices (Splice AppHandler)
attrExampleSplice = "foo" ## return $ yieldPureText "hello"
Caution: attribute substitution operates at the Builder
level. It
can be a vector for injection attacks and care should be taken if you
use any user input as attribute values. Neither does it care whether
you splice in a node instead of a simple text splice.
Let's have an example of a common need, percent escaping (also known as URL escaping). I suspect there may be better ways to do this if you are reading this in the future. Strictly speaking this is out of scope for a Heist tutorial but I feel that I'd be amiss if I didn't work out an example of this.
import Network.HTTP.Types.URI (Query, encodePath)
import Blaze.ByteString.Builder.Char.Utf8 (fromString)
exampleQuery :: Query
exampleQuery =
[ ("simple", Just "foo")
, ("escapes", Just "äö")
, ("novalue", Nothing)
]
examplePath :: [Text]
examplePath = [""]
examplePathBuilder :: Builder
examplePathBuilder = encodePath examplePath exampleQuery
querySplice :: Splices (Splice AppHandler)
querySplice = "href" ## yieldPure $ (fromString "http://example.com") <> examplePathBuilder
Using attribute substitution provides no way of omitting the attribute completely and the best you could do with it is to end up with empty string for the value. Heist offers attribute splices to take care of this scenario.
<h:attrsplice>
<select name="select">
<option h:maybeselected="red">Red</option>
<option h:maybeselected="green">Green</option>
<option h:maybeselected="blue">Blue</option>
</select>
</h:attrsplice>
The key to defining attribute splices is withLocalSplices
. Note how
we access state to use for the attribute splice from the application
state.
-- Defintion from Application.hs
data App = App
{ _heist :: Snaplet (Heist App)
, _appData :: Text
}
attrSpliceSplices :: Splices (Splice AppHandler)
attrSpliceSplices = "attrsplice" ##
withLocalSplices mempty optionAttrSplices runChildren
optionAttrSplices :: Splices (AttrSplice AppHandler)
optionAttrSplices =
"maybeselected" ## \v -> do
preferred <- lift $ view appData
let selected = if v == preferred
then [("selected", "")]
else []
return $ ("value", v) : selected
It's also possible to use data from runtime splices' state for attribute splices. See the source code for an example.
Note that this is nothing that couldn't be done by having a splice generate each option tag itself. It's up to you to use either based on what feels most natural for the situation.
The final attribute related functions are:
runAttributes :: Monad n => [(Text, Text)] -> HeistT n IO [DList (Chunk n)]
runAttributesRaw :: Monad n => [(Text, Text)] -> HeistT n IO (RuntimeSplice n [(Text, Text)])
These can be used to trigger resolution of attribute splices manually.
Internally in Heist compileNode
uses runAttributes
to turn a
Node
's attributes to the chunks representing key="value"
pairs of
a node's attributes. This is a pretty opaque form for them and you
can't do much with it beyond outputting them. The runAttributesRaw
version is likely to be the more useful of the two.
You'll most likely get the attribute values to give to these functions
by using getParamNode
.
getParamAttrs :: Monad m => HeistT n m [(Text, Text)]
getParamAttrs = elementAttrs <$> getParamNode
These two functions are unlikely to see any use in your program, but they're there to perform an operation that's otherwise hidden, if you ever needed it.
By now, you may have noted a repetitive pattern in the type
definitions: RuntimeSplice n a -> Splice n
. As stated previously,
n
is the runtime type, which typically is Handler App App
(or
AppHandler
) when used with Snap. It should be instructive to make
one further type alias and see what the relevant Heist functions'
types look like when written in terms of that.
type RuntimeAppHandler a = RuntimeSplice AppHandler a -> Splice AppHandler
deferMany :: (Foldable f, Monad n) => RuntimeAppHandler a -> RuntimeSplice n (f a) -> Splice n
defer :: Monad n => RuntimeAppHandler a -> RuntimeAppHandler a
deferMap :: Monad n => (a -> RuntimeSplice n b) -> RuntimeAppHandler a -> RuntimeAppHandler a
mayDeferMap :: Monad n => (a -> RuntimeSplice n (Maybe b)) -> RuntimeAppHandler a -> RuntimeAppHandler a
bindLater :: Monad n => (a -> RuntimeSplice n Builder) -> RuntimeAppHandler a
withSplices :: Monad n => Splice n -> Splices (RuntimeAppHandler a) -> RuntimeAppHandler a
manyWithSplices :: (Foldable f, Monad n) => Splice n -> Splices (RuntimeAppHandler a) -> RuntimeSplice n (f a) -> Splice n
manyWith :: (Foldable f, Monad n) => Splice n -> Splices (RuntimeAppHandler a) -> Splices (RuntimeSplice n a -> AttrSplice n) -> RuntimeSplice n (f a) -> Splice n
As far as naming goes, I'm not sure whether "RuntimeAppHandler" is a good one for this synonym. I've coined it for this tutorial but I'm open for suggestions for a more descriptive one.
Interpreted Heist predates compiled Heist. As such, there may be material around which talks about interpreted Heist's features without specifying that it may be missing or in a different form in compiled Heist.
You may see them use splices such as bind
, apply
and
apply-content
. If you are using only compiled Heist you won't have
those available. I'm not covering mixed use in this tutorial but see
hcLoadTimeSplices
should you have an interest in that. For an
example of how to define apply
and apply-content
for compiled
Heist, see example4
.
Code which may mix interpreted and compiled Heist typically uses
qualified imports I
and C
, respectively. This tutorial only uses
compiled Heist and therefore it omits using the C
.