Skip to content

Migrating Snap Applications to Heist 0.10

mightybyte edited this page Jan 16, 2013 · 6 revisions

Imports

The first step in migrating to Heist 0.10 is to change your imports. Since Heist 0.10 is practically a ground-up rewrite and introduces changes that aren't backwards compatible, we decided to take the opportunity to clean up the module hierarchy. We got rid of the old hierarchical Text.Templating.* module layout. The new typical import list for Heist is:

import           Heist
import qualified Heist.Interpreted as I
import qualified Heist.Compiled as C

Heist.Interpreted and Heist.Compiled export some of the same symbols, so if you import them both, you need to make one or both of them qualified. We recommend just making them both qualified to increase readability.

If you're not interested in using compiled Heist to take advantage of the speed improvement, then you can use a different set of imports that will have the smallest impact on existing code:

import Heist
import Heist.Interpreted

The following simple regular expression that will take care of the bulk of the import changes.

s/Text.Templating.Heist/Heist/

Splices

If you use the first set of imports above, there are a number of code changes you'll have to make. The following substitutions are the most common.

s/Splice/I.Splice/
s/textSplice/I.textSplice/

GHC will inform you of any other code changes that need to be made due to qualified names. :D

SnapletHeist

The snap package no longer defines a SnapletHeist monad to wrap Heist computations in a snaplet context. All use of liftHeist can be removed entirely. You can use the following substitutions for the other functions:

s/liftHandler/lift/
s/liftAppHandler/lift/

The consequence of this change is that splices don't automatically get put in the appropriate snaplet context. Since the Heist snaplet requires that splices use the Handler b b monad, snaplets that automatically bind splices will have to pass a lens into the splice so it can do the appropriate with call if any kind of snaplet data is needed. Note that this only affects you if you are writing a general purpose snaplet designed to work in any context. If you only care about the current application, then you can just use the appropriate lenses to access the data you need rather than threading them through. The benefit of this change is that there is no longer the problem of needing to run HeistT functions in the SnapletHeist monad. Another common substitution you might see is this:

s/SnapletHeist App App Template/SnapletISplice App/

Templates

Once you get your application building, you will probably need to make some changes to your templates. If you used <content> tags anywhere inside your apply tags or bind tags, they need to be renamed to <apply-content> and <bind-content> respectively. We did this to avoid the namespace clash and allow you to use content from bind and apply tags even when they are nested. Note that Heist 0.10 will automatically check this and alert you if any of your templates have the old style <content> tags anywhere when you start up your application.

Intermission

At this point, your application should build and work like it did before. You've upgraded to Heist 0.10, but you're not taking advantage of the new functionality and much faster template rendering. Now you should assess whether and what parts of your application can be changed to use compiled templates. There are a few points you should keep in mind. If you thoroughly understand the difference between compiled splices and interpreted splices, these points may be obvious, but we'll try to summarize the main ideas.

heistLocal

You cannot use heistLocal with compiled templates. Period, end of story. That is the core tradeoff that compiled splices exploit to get better performance. All your splices must be defined up front. In the future, we may try to make it possible to do a hybrid of compiled and interpreted templates, but first we want to see how much this impacts real-world development.

So you need to look through your application and find every place where you use one of the following functions:

  • heistLocal
  • withSplices
  • renderWithSplices

You won't necessarily have to get rid of these functions, but if you keep them, you'll have to make sure that they are being used in conjunction with interpreted template rendering (i.e. render, renderAs, heistServe, or heistServeSingle). If you use one of the above three functions but are rendering those templates with any of the Heist snaplet functions with a "c" prefix, then the dynamically bound splices simply won't work.

runChildren

The runChildren family of functions used to write interpreted splices has a compiled splice dual family called promiseChildren.

Lenses

In 0.10, the snap package also switched to use Edward Kmett's new lens library instead of data-lens. This allows us to avoid having to import Control.Category and hide the Prelude version of the (.) function. You can do this with something similar to the following regexes:

s/import.*Control.Category.*//
s/import.*Prelude hiding.*(.).*//

Then, replace all Data.Lens.* imports with a single

import Control.Lens

The catch is that lens composition now works in the opposite order. So if you had two lenses composed with (a . b) before, you'll have to switch that to (b . a). Also, the makeLenses call changes slightly from:

makeLenses [''App]

to

makeLenses ''App

Splice Functions

With the interpreted splices, functions such as Person -> I.Splice m are common. I discussed this pattern awhile back in a blog post. With compiled splices you have to change your mindset a bit.

personSplice :: Person -> C.Splice m

The function personSplice is completely useless! To see why, think about the meaning and timing of things. C.Splice m is a computation that runs at load time. But Person is almost definitely going to be data that is only known at run time. A function a -> b means that you have to have a in order to get b. So the personSplice definition above says that if you have a run time piece of information, it will give you back something that is only useful at load time. If you want to do something useful with dynamic data and compiled splices, you have to write the function differently.

personSplice2 :: Promise Person -> C.Splice m

This says that in order to get a load time splice for a person, you have to give it a promise for a person that will be calculated in the future. It can be a bit of a mental switch to get used to working with splices in this way.

Promises are a pretty low-level concept. You can only work with them in the HeistT and RuntimeSplice monads. A higher level abstraction that will probably be more useful is something like this.

personSplice3 :: m Person -> C.Splice m

This says that in order to get a load time splice for a person, you have to give it a runtime computation that can get the person.