Skip to content

Compiled Splice Formulations

Doug Beardsley edited this page Jul 1, 2013 · 6 revisions

The old interpreted splices were relatively easy to work with and reason about. Everything was happening at runtime and by default all tags were always evaluated for splices. Compiled splices are more involved because we're trying to do as much work as possible at load time while still having access to run time data. It's useful to look at some of the ways we might construct splices from run time data. I'll use the type variable a to denote the runtime data that is being used to construct splices.

a -> [(Text, Splice m)]

This is the old interpreted style of splice. It's the Post type that I talked about in my blog post Looping and Control Flow in Heist. In that post I was essentially using this formulation (even though the type signatures didn't look like it). I had an a from which I could construct a list of splices embodying derived concepts you might want to use in templates.

This splice formulation won't work because you cannot bind compiled splices on the fly. The names of all your top-level splices must be known at load time. But with a formulation like this, we can't get those names until run time.

[(Text, a -> Builder)]

To get around the previous problem, we have to move the a inside to the second element of the tuple. Instead of generated DOM trees of nodes like we did in interpreted mode, now Builder is our most basic unit of operation. The efficiency of compiled splices comes from just concatenating things together and the Builder structure is optimized for this. From now on we'll call this the "pure compiled splice formulation" or just "pure formulation" when it's clear that we're talking about compiled splices.

It's quite trivial to build some helper functions that let you convert from other more convenient variants of this pure formulation to this one. All it takes is a function like this:

textSplices :: [(Text, a -> Text)] -> [(Text, a -> Builder)]

And now you can work with functions that generate Text.

The problem with the pure formulation is that it is too simple and doesn't give us enough power. Consider the following data type.

data User = User
    { userName :: Text
    , userIsAdmin :: Bool
    }

From this user type, you might want to create the following splices.

[ ("userName", userName)
, ("userIsAdmin", show . userIsAdmin)
, ("ifUserIsAdmin", ...)
]

In this case, the ifUserIsAdmin splice would only render its children if the user was an administrator. This would be useful if you wanted to only show certain menu options to administrator users. The problem is that you can't express this behavior in the type a -> Builder. You have to have access to the spliced node bound to the ifUserIsAdmin tag.

[(Text, a -> HeistT n IO Builder)]

To remedy the problem, we change the formulation so that the desired piece of code is running in the HeistT n IO monad. But this formulation has a subtle problem. Things of type HeistT n IO are only useful at load time. But we've already defined that a is only available at run time. More generally, any time you have a function of the form runtime -> loadtime that function is completely useless because you have to wait until runtime before you'll have the data that lets you calculate the load time thing.

Therefore it might make sense to want to write this function, and it will even type check, but you'll never be able to actually use this function. So we have to think of something better. Let's try the next most general thing.

[(Text, n a -> HeistT n IO Builder)]

Now we're getting somewhere. n a is not a value known only at runtime. It's a monadic action telling us how to compute a value at runtime. This is something we can know at load time, so it is something we can actually use. Heist provides the following function in its API. (Splice n is a type alias that is roughly equivalent to HeistT n IO Builder.)

withLocalSplices :: [(Text, Splice n)]
                 -> [(Text, AttrSplice n)]
                 -> HeistT n IO a
                 -> HeistT n IO a

Using that we can easily write a function like this.

runChildrenWith :: Monad n => [(Text, n a -> Splice n)] -> n a -> Splice n

Or you could just pass the n a to the second part of each tuple by hand. But this formulation has a problem. It can't be used to write a function like this.

mapChildrenWith :: Monad n => [(Text, n a -> Splice n)] -> n [a] -> Splice n

The impossibility of this function might not be immediately obvious. But I think you'll find that I am correct. It is impossible for the same reason that the a -> HeistT n IO Builder formulation was impossible. If all you have is n [a], then you cannot construct an n a until runtime. And you're back to square one.

[(Text, IORef a -> HeistT n IO Builder)]

What we need is a function that has a hole in which we can put successive elements of our list at runtime. An IORef provides just that. The IORef a is something we can construct at load time which will then allow us to bind the splice we want. Then we just store our elements into the IORef at runtime, run said splice, and repeat for each element of the list.

But this has a problem. Our web server is multithreaded. If we construct an IORef at load time, then every user of our website will be using the same reference whenever they hit a page with this splice. It's very possible that two users could hit the page almost simultaneously and user B would overwrite the data user A had stored in the IORef before user A gets a chance to read it. So this solution won't work either.

[(Text, Promise a -> HeistT n IO Builder)]

Now we come to our final solution. We have constructed the notion of a Promise that behaves kind of like an IORef, but is backed by a different set of data for each user. This makes it so that multiple users accessing the same page at the same time can never overwrite each other's data, and is the final form that compiled splice functions must take. You may not always work with this exact type signature, but you will always use the underlying idea.