Introduction to Compiled Heist
Before version 0.10, Heist has essentially been an interpreter. It loads your templates and “runs” them whenever a page is served. This is relatively inefficient since a lot of document transformations happen every time the template is requested. For Heist version 0.10 we completely rethought everything with performance in mind. We call it “compiled Heist”. The main idea is to do most of your splice processing up front at load time. There is still a mechanism for rendering dynamic information at runtime, but it is faster than the fully interpreted approach that Heist started with.
It should also be mentioned that the old “interpreted Heist” is not gone. You can still use the old approach where all the transformations happen at render time. This allows you to upgrade without making sweeping changes to your code, and gradually convert your application to the more performant compiled approach as you see fit.
Before we continue it should be mentioned that you are reading real live literate Haskell code from our test suite. All the code you see here is compiled into our test suite and the results automatically checked by our buildbot. So first we need to get some boilerplate and imports out of the way.
{-# LANGUAGE NoMonomorphismRestriction #-}
module Heist.Tutorial.CompiledSplices where
import Heist
import qualified Heist.Compiled as C
import Heist.Tutorial.Imports
import Control.Applicative
import Control.Lens
import Data.Map.Syntax
As a review, normal (interpreted) Heist splices are defined like this.
type Splice m = HeistT m m [Node]
The type parameter m
is the runtime execution monad (in a Snap application this will usually be Handler
or Snap
). Don’t worry about why the m
is there twice right now. We’ll get to that later. The splice’s return value is a list of nodes that is substituted back into the document wherever the spliced node was.
This kind of splice proccessing involves traversing the DOM, which is inefficient. Compiled Heist is designed so that all the DOM traversals happen once at load time in the IO monad. This is the “compile” phase. The type signature for compiled splices is this.
type Splice n = HeistT n IO (DList (Chunk n))
We see that where Heist splices ran in the m monad, compiled splices run in the IO monad. This also explains why HeistT now has two monad type parameters. The first parameter is a placeholder for the runtime monad and the second parameter is the monad that we’re actually running in now.
But the key point of the compiled splice type signature is the return value. They return a DList of Chunks. DList is a list that supports efficient insertion to both the front and back of the list. The Chunk type is not exposed publicly, but there are three ways to construct a Chunk.
yieldPure :: Builder -> DList (Chunk m)
yieldRuntime :: RuntimeSplice m Builder -> DList (Chunk m)
yieldRuntimeEffect :: Monad m => RuntimeSplice m () -> DList (Chunk m)
If your splice output can be calculated at load time, then you should use yieldPure
or one of its variants. When you do this, Heist can concatenate all adjacent pure chunks into a single precalculated ByteString that can be rendered very efficiently. If your template needs a value that has to be calculated at runtime, then you should use the yieldRuntime
constructor and supply a computation in the RuntimeSplice monad transformer that is parameterized by m
which we saw above is the runtime monad. Occasionally you might want to run a runtime side effect that doesn’t actually insert any data into your template. The yieldRuntimeEffect
function gives you that capability.
An Example
With that background, let’s get to a real example.
stateSplice :: C.Splice (StateT Int IO)
stateSplice = return $ C.yieldRuntimeText $ do
val <- lift get
return $ pack $ show (val+1)
Here we see that our splice’s runtime monad is StateT Int IO
. This makes for a simple example that can clearly demonstrate the different contexts that we are operating in. To make things more clear, here’s a version with some print statements that clarify the details of which monad is executed when.
stateSplice2 :: C.Splice (StateT Int IO)
stateSplice2 = do
-- :: C.Splice (StateT Int IO)
lift $ putStrLn "This executed at load time"
let res = C.yieldRuntimeText $ do
-- :: RuntimeSplice (StateT Int IO) a
lift $ lift $ putStrLn "This executed at run/render time"
val <- lift get
return $ pack $ show (val+1)
lift $ putStrLn "This also executed at load time"
return res
Note here that even though the type parameter to C.Splice is a monad, it is not a monad transformer. RuntimeSplice, however, is. Now let’s look at a simple load function that sets up a default HeistState and loads templates from a directory with compiled splices.
load :: MonadIO n
=> FilePath
-> Splices (C.Splice n)
-> IO (HeistState n)
load baseDir splices = do
tmap <- runExceptT $ do
let sc = mempty & scLoadTimeSplices .~ defaultLoadTimeSplices
& scCompiledSplices .~ splices
& scTemplateLocations .~ [loadTemplates baseDir]
ExceptT $ initHeist $ emptyHeistConfig & hcNamespace .~ ""
& hcErrorNotBound .~ False
& hcSpliceConfig .~ sc
either (error . concat) return tmap
Here’s a function demonstrating all of this in action.
runWithStateSplice :: FilePath
-> IO ByteString
runWithStateSplice baseDir = do
hs <- load baseDir ("div" ## stateSplice)
let runtime = fromJust $ C.renderTemplate hs "index"
builder <- evalStateT (fst runtime) 2
return $ toByteString builder
First this function loads the templates with the above compiled splice. You have to specify all the compiled splices in the call to loadTemplates because loadTemplates takes care of compiling all the templates up front. If you were able to bind compiled splices later, then all the templates would have to be recompiled, a potentially expensive operation. Next, the function renders the template called “index” using a runtime (StateT Int IO) seeded with a value of 2 and returns the resulting ByteString.
Now let’s look at a more complicated example. We want to render a data structure with a compiled splice.
data Person = Person
{ pFirstName :: Text
, pLastName :: Text
, pAge :: Int
}
personSplices :: Monad n
=> Splices (RuntimeSplice n Person -> C.Splice n)
personSplices = mapV (C.pureSplice . C.textSplice) $ do
"firstName" ## pFirstName
"lastName" ## pLastName
"age" ## pack . show . pAge
peopleSplice :: (Monad n)
=> RuntimeSplice n [Person]
-> C.Splice n
peopleSplice = C.manyWithSplices C.runChildren personSplices
allPeopleSplice :: C.Splice (StateT [Person] IO)
allPeopleSplice = peopleSplice (lift get)
personListTest :: FilePath
-> IO ByteString
personListTest baseDir = do
hs <- load baseDir ("people" ## allPeopleSplice)
let runtime = fromJust $ C.renderTemplate hs "people"
builder <- evalStateT (fst runtime)
[ Person "John" "Doe" 42
, Person "Jane" "Smith" 21
]
return $ toByteString builder
Disadvantages of Compiled Heist
Compiled Heist is faster than the original interpreted approach, but as with most things in computing there is a tradeoff. Compiled Heist is strictly less powerful than interpreted Heist. There are two things that compiled Heist loses: the ability to bind new splices on the fly at runtime and splice recursion/composability.
The first point follows immediately from the definition of compiled Heist. When you decide to do all your splice DOM traversals once at load time you’re unavoidably limited to only those splices that you defined at load time. But this seems to be a good pattern to use in general because debugging your splices will be easier if you don’t have to consider the possibility that the handler that binds them didn’t run.
The loss of recursion/composability happens because of the change in the type signature of splices. Interpreted splices are a essentially function [Node] -> m [Node]
. This means that the output of one splice can be the input of another splice (including itself). Compiled splices are a function [Node] -> IO (DList (Chunk m))
. Therefore, once a splice processes some nodes, the output is no longer something that can be passed into other splices.
This composability turns out to be a very powerful feature. Head merging is one feature that can’t be done without it. Head merging allows you to put <head>
tags anyhere in any template and have them all merged into a single <head>
tag at the top of your HTML document. This is useful because it allows you to keep concerns localized. For instance, you can have a template represent a small piece of functionality that uses a less common javascript or CSS file. Instead of having to depend on that resource being included in the top-level <head>
tag, you can include it in a <head>
tag right where you’re using it. Then it will only be included on your pages when you are using the markup that needs it.
Our implementation of head merging uses a splice bound to the <html>
tag. This splice removes all the <head>
nodes from its children, combines them, and inserts them as its first child. This won’t work unless the <html>
splice first runs all its children to make sure all <apply>
and <bind>
tags have happened first. And that is impossible to do with compiled splices.
To get around this problem we added the concept of load time splices. Load time splices are just interpreted splices that are completely executed at load time. If interpreted splices have type [Node] -> m [Node]
where m is the runtime monad, then load time splices have type [Node] -> IO [Node]
, where IO is the monad being executed at load time. Load time splices give you the power and composability of interpreted splices as long as they are performing transformations that don’t require runtime data. All of the built-in splices that we ship with Heist work as load time splices. So you can still have head merging by including our html splice in the load time splice list in your HeistConfig.
A More Involved Example
The person example above is a very common and useful pattern for using dynamic data in splices. But it has the simplification that it always generates output the same way. Sometimes you might want a splice’s output to have one form in some cases and a different form in other cases. A simple example is a splice that reads some kind of a key from a request parameter then looks that key up in some kind of map. If the key is present the splice uses its child nodes as a view for the retrieved value, otherwise it outputs an error message.
This pattern is a little tricky because you’re making decisions about what to render based on runtime data, but the actual rendering of child nodes has to be done at load time. To bridge the gap and allow communication between load time and runtime processing we provide the Promise data type. A Promise is kind of like an IORef except that operations on them are restricted to the appropriate Heist context. You create a new empty promise in the HeistT n IO (load time) monad, and you operate on it in the RuntimeSplice monad.
Here’s an example of how to use a promise manually to render a splice differently in the case of failure.
failingSplice :: MonadSnap m => C.Splice m
failingSplice = do
promise <- C.newEmptyPromise
outputChildren <- C.withSplices C.runChildren splices (C.getPromise promise)
return $ C.yieldRuntime $ do
-- :: RuntimeSplice m Builder
mname <- lift $ getParam "username"
let err = return $ fromByteString "Must supply a username"
single name = do
euser <- lift $ lookupUser name
either (return . fromByteString . encodeUtf8 . T.pack)
doUser euser
where
doUser value = do
C.putPromise promise (name, value)
C.codeGen outputChildren
maybe err single mname
splices :: Monad n
=> Splices (RuntimeSplice n (Text, Text) -> C.Splice n)
splices = mapS (C.pureSplice . C.htmlNodeSplice) $ do
"user" ## (:[]) . TextNode . fst
"value" ## (:[]) . TextNode . snd