Monday, April 18, 2011

Splice Subtleties

In the recent release of Heist 0.5.1, there was performance bug in my implementation of head merging. I turned off the head merging, and then fixed the bug but did not reenable head merging. If you want to turn it back on, you need to bind the html tag to the htmlImpl function from the Text.Templating.Heist.Splices.Html module. I think I'll continue to leave it off by default because it does have some potential to impact performance.

While analyzing the problem, I realized that it involves fairly obscure details of Heist internals that have not been mentioned in any of our tutorial materials. And if I, the original author of Heist fell prey to this problem, it's probably safe to assume that eventually someone else will too. So let's take a look at some details of Heist's internal behavior and the implications for splice developers.

As I alluded to in Heist in 60 Seconds, you can think of a splice as follows:

type Splice = Node -> m [Node]

The actual implementation is slightly different because we use a reader monad for the parameter node, but this is a useful mental model.

Heist is kind of like one big fmap on the whole DOM tree. If there were no splices bound, the fmapped function would be id. When you bind a splice to a tag <foo>, the splice function is applied to every occurrence of that tag. The whole tag and all its children become the splice's parameter, and they are replaced by the output of the splice in the rendered document.

Splices can be divided into two broad categorizations: substitutions and filters. Substitution splices are where the spliced tag disappears in the output. If the spliced tag is preserved in the output, then it's a filter splice.

Substitution splices probably the most common. All uses of the bind tag are substitutions. But my initial implementation of head merging in Heist 0.5.1.0 used a filter splice bound to the html tag. If you're writing a filter splice, you MUST call the stopRecursion function somewhere in the splice or you'll get either wrong or slow behavior! If you're not interested in the details behind this, you can stop reading now. Otherwise, let's look at the function Heist uses to process nodes.
runNode :: Monad m => X.Node -> Splice m
runNode (X.Element nm at ch) = do
    newAtts <- mapM attSubst at
    let n = X.Element nm newAtts ch
    s <- liftM (lookupSplice nm) getTS
    maybe (runKids newAtts) (recurseSplice n) s
  where
    runKids newAtts = do
        newKids <- runNodeList ch
        return [X.Element nm newAtts newKids]
runNode n                    = return [n]
The highlighted lines are the important ones here. First it does a lookup to see if there are any splices bound to the tag that we're currently processing. Then, if we found a splice, we call recurseSplice and pass it the current node. Here's the code for recurseSplice.
recurseSplice :: Monad m => X.Node -> Splice m -> Splice m
recurseSplice node splice = do
    result <- localParamNode (const node) splice
    ts' <- getTS
    if _recurse ts' && _recursionDepth ts' < mAX_RECURSION_DEPTH
        then do modRecursionDepth (+1)
                res <- runNodeList result
                restoreTS ts'
                return res
        else return result
  where
    modRecursionDepth :: Monad m => (Int -> Int) -> TemplateMonad m ()
    modRecursionDepth f =
        modifyTS (\st -> st { _recursionDepth = f (_recursionDepth st) })
The first highlighted line here executes the splice. The second one runs all the result nodes--which executes all the splices again. If the splice is a filter splice, then the the same splice will appear in the result list and you'll get infinite recursion.

As you can see, Heist does have checks to detect this situation and terminate if the recursion gets too deep. There is also a flag that can be set to prevent recursion altogether. You can set this flag inside your splices by calling the stopRecursion function. Recursion is turned on by default because that's what you usually want to happen for substitution splices. But remember this, and make sure you call stopRecursion if you're writing filter splices!

No comments: