Type-safe runtime Hamlet

August 8, 2010

GravatarMichael Snoyman

Current Hamlet inclusion options

I recently released Hamlet version 0.4.2, which added an often-requested feature: runtime Hamlet templates. This allows some really cool possibilities, such as using Hamlet templates in Hakyll. There are now three different ways to include a Hamlet template in your code; let's look at the options and their relatives strengths and weaknesses.

  • Quasi-quoted
    • Advantages
      • Fully type-checked at compile time.
      • Changing the template automatically forces a recompile.
      • Does as much work as possible at compile-time; faster runtime execution.
    • Disadvantages
      • Some people like to keep their templates and code separate.
      • The only way to see the result of a template change is a recompile- slows down development cycle.
  • Template Haskell external file
    • Advantages
      • Fully type-checked at compile time.
      • Does as much work as possible at compile-time; faster runtime execution.
      • Keeps templates and code separate.
    • Disadvantages
      • Changing the template does not automatically forces a recompile; you might see stale content if you're not careful.
      • The only way to see the result of a template change is a recompile- slows down development cycle.
  • Runtime templates
    • Advantages
      • Immediately see results of template changes, without a recompile.
      • Keeps templates and code separate.
    • Disadvantages
      • Not type-checked at all.
      • Parse errors only appear at runtime.

In addition, it's very difficult to get runtime templates to interact well with the first two options. In the case of Yesod, there is basically no machinery in place to help you out; you'll have to write it all yourself.

The Fourth Option

However, Hamlet 0.5 is going to include a fourth method which will have the following characteristics:

  • Advantages
    • Fully type-checked at compile time.
    • Is code-wise identical to the external Template Haskell method.
    • You can view changes to your template without a recompile. If you have made changes the break the type-safety of your template, you will get an error message and be informed you must recompile.
    • You can use this method during testing and easily switch all of your code to the external TH version for production, thereby avoiding all runtime costs.
  • Disadvantages
    • Since you are avoiding a compilation step, sometimes you'll need to manually initiate a recompile.
    • Some of the more advanced Hamlet tricks available to quasi-quoted and TH templates are not available. This shouldn't affect most people.

Half runtime, half Template Haskell, and a little unsafe

Let's say that I have the simple Hamlet template Hello $name$. If we use either quasi-quoted or TH Hamlet, name will be converted into an Exp and will reference the variable in scope called name. If we use runtime Hamlet, we would need an appropriate HamletData value, something looking like HDMap [("name", HDHtml $ string name)].

Now let's say that we want to change that template to Hello $name$!. All I've done is added an exclamation point, so I know the type safety of the template has not been affected. Here are the results for the three inclusion options.

Quasi-quoted
Fully recompile the entire module containing the template.
Template Haskell
The change won't appear until you manually force the module to be recompiled.
Runtime
Change appears immediately without a recompile.

However, let's say that I change the template now to:

%ul
    $forall names name
        %li Hello $name$

I also immediately fix up my Haskell code so that instead of name = "Michael" I have names = words "Michael Miriam Eliezer Gavriella". Now, the results are:

Quasi-quoted
Fully recompile the entire module containing the template.
Template Haskell
Fully recompile the entire module containing the template.
Runtime
Code won't compile until you also modify the HamletData value to something like HDMap [("names", HDList $ map (HDHtml . string) names)].

So with options 1 and 2, we get an unnecessary recompile step in case 1. In case 2, we require Haskell code changes, so the recompile is unavoidable. However, it's much more tedious to make the code changes for option 3. Also, let's say that we made the Haskell code change (name to names) before the template changes. Options 1 and 2 would give us a compile-time error message, while option 3 would only complain at runtime.

What we really want is to have that HamletData value constructed for us by Hamlet, by reading the template file at compile time. However, we also want it to read the template file at runtime and apply the HamletData value to it. This is exactly what option 4 does.

How it works

There's a new function in Hamlet 0.5: hamletFileDebug. (Don't get attached to any names, I expect a lot of renamings before it's released.) It has an identical signature to hamletFile (the external Template Haskell version): FilePath -> Q Exp. hamletFile pulls in the template from the given file at compile time, parses it and converts it into Haskell code that gets compiled. Template Haskell changes the Q Exp into a value Hamlet url.

hamletFileDebug is slightly different. It also pulls in the template from the file at compile time and parses it. Next, it scans through the parsed template and finds all references to variables and how they are used. Using the example above, it would notice that the template refers to name and expects it to be a String. It then creates the appropriate HamletData value based on all these references.

Like hamletFile, hamletFileDebug will also return a value of type Hamlet url. However, this value will in fact read the specified file at runtime and render it against the HamletData value each time. In order to achieve this, it has to use unsafePerformIO; since this is intended to enhance your development, and should not be used in production, I think it's a fair use.

How it plays out

So how does this interact with a normal Hamlet development workflow? Let's go back to the name to names example: if you change your Haskell code first, hamletFileDebug will get called again during your recompile and will inspect the template. It will notice that the name variable is no longer in scope, and you will get a compile-time error.

On the other hand, if you change the Hamlet template file first, you'll end up with a runtime error. However, I think it's a fair trade-off: you're skipping a compile cycle here, so the only way to get an error message is at runtime.

Looking at the exclamation mark example, no code changes are required, only template changes. In this case, your changes will become immediately visible.

Next steps

I'm very excited about how this change will affect my development cycle; it will make it much easier to play around with HTML changes to see the results. This also converges with some other work I've been doing: Stylish. I created a github repo for Stylish a few days ago, which is meant to be Hamlet for CSS. It is based on the recently released blaze-builder, which will also be the core of blaze-html 0.2.

Even though Yesod offers an addStyle function for including CSS declarations, I almost always use static CSS files for this purpose, simply because I can't afford to go through a whole compile cycle to test out changing the border from 2px to 3px. (I know about firebug, don't worry.) However, Hamlet 0.5 should hopefully solve this problem: by using the same technique of hamletFileDebug, I'll be able to test out style changes immediately.

So now I'm announcing the inclusion of two more pieces in Hamlet 0.5: Camlet and Jamlet. They are to CSS and Javascript, respectively, what Hamlet is to HTML. Camlet is going to be what Stylish is right now: white-space sensitive CSS allowing you to embed variables from Haskell and nest CSS declarations. I think Jamlet will simply be a text pass-through for the moment, but may eventually support Javascript compression.

By using these two libraries with Hamlet, you won't need to worry about deploying your static files separately. And here's the really cool feature I'm hoping to add to Yesod 0.5: caching. When you use addStyle in Yesod, it will concatenate all of the CSS declarations for a page together. It will then take a hash of that value, cache the value based on the hash, and send a link tag referencing that content via the hash. When that content is served, it will have an expiration date set long in the future; since the hash value will automatically change whenever you change the CSS, you won't need to worry about users getting stale CSS files. The same will be true for Javascript.

This will also give you a number of minor benefits. For example, instead of hard-coding CSS names in your HTML, CSS and JavaScript separately, you could declare the name once in Haskell and reference that variable throughout. You'll be alerted at compile time if you've made a typo in a CSS class name. You're also guaranteed to have well-formed CSS files.

I'm very excited about all of these changes, and hope to be making these releases soon. Let me know your thoughts on this earlier, rather than later, if possible. Along with the database migration additions, I think Yesod 0.5 is going to be a very fun release.

Comments

comments powered by Disqus

Archives