Skip to content

Latest commit

 

History

History
718 lines (561 loc) · 27.3 KB

tutorial.md

File metadata and controls

718 lines (561 loc) · 27.3 KB

The Heist tutorial I wish I'd had when I started

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.

What is "compiled" in compiled Heist

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.

Types

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.

What is a splice

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.

Initializing the Heist Snaplet

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.

Splices to runtime and back

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.

Pure splice functions

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

Creating new splices

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

Constructing Chunks

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")

Manipulating the map of splices

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.

withSplices and pureSplice . textSplice

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")

Functions manipulating runtime splices

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

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.

Attributes

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.

Percent escaping

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

Attribute splices

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.

Resolving attribute splices manually

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.

Types, simplified with an alias

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.

Caution about other tutorials and interpreted Heist

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.

Footnotes

  1. Four but one is deprecated