What's in a Hamlet?
August 1, 2010
Quick message from our sponsors: I just wanted to inform everyone that I'm nearing a new release of Yesod, version 0.5. This release introduces very few changes versus 0.4, notably some enhancements to forms. The main reason for this release is to upgrade to the (not yet released) persistent 0.2, which adds some major features, most notably database migrations. I encourage everyone to check out the code on github.
What's in a Hamlet?
Hamlet- for those not familiar- is an HTML templating system for Haskell with a Haml-inspired syntax. It is quasi-quoted, meaning it is fully parsed and converted to Haskell code and checked at compile time with the rest of your code. (I'm currently toying with the idea of adding support for runtime-parsed templates, but that is quite orthogonal to this discussion.)
Hamlet is built on top of BlazeHtml, and can be called in Haskell code as follows:
myTemplate = [$hamlet| %h1 Hello World! %p This is some static stuff. %ul $forall people person %li $person$ |]
Now, given everything I've just said, you might guess that the type signature for myTemplate is
myTemplate :: Html (); however, you'd be wrong. The actual signature is in fact
myTemplate :: (a -> String) -> Html (). There's a function (hamlet') in the Hamlet repo which will most likely be included in the next release which would create the former type signature, but for now I want to focus on the latter signature.
Coding PHP in Haskell
I'm not trying to pick on PHP here (well, maybe a little...). Let's say that I want to create a link in a PHP web app. I'm sure you're used to seeing code like
<a href="person.php?person_id=<? echo $person_id;?>">Check out this guy</a>. I'm sure many web developers can remember getting 404 errors because they forgot to update all their URLs when they renamed person.php to people.php.
I was serious: I don't want to pick on PHP here. This was a problem back in the days of Perl CGI scripts, or if you were aggressive enough, C CGI scripts as well. For that matter, it's a bane when writing static HTML as well.
A partial solution
Many (most?) web frameworks try to deal with this problem. My largest pre-Haskell web experience was with Django, so I'll speak to their approach. Caveat emptor: I stopped using Django before they hit 1.0, so my information and terminology is probably dated. Over there, when you declare a route, you can also give a name to that route. You declare routes in Django via regular expressions; you then have utility functions that take a named route and pieces to "fill the gap" in the regular expressions, and thus URLs are born.
With this approach, it doesn't matter if you change "/person/(\d+)/" to "/people/(\d+)/"; as long as the name stays the same, your code will still point to the right URL. Thus a whole class of annoying 404 bugs are eliminated, and the gods were pleased.
But it's still dynamic
I'll assume I'm preaching to the choir here: dynamic languages don't give you a lot of safety assurances. The "compiler" will happily accept
So I ask you fellow Haskell web developers: why do you demand such safety in function calls, but not in URL construction? The problem with both the URL-splicing approach and the Django (et al) approach is a lack of compile-time safety!
I'll admit that original versions of Yesod had the same flaw; fortunately, I was pointed in the right direction and now type-safe URLs are a cornerstone of Yesod. The concept is unbelievably simple, and very obvious once stated: we should have a datatype represent URLs. Every URL our application accepts should correlate to some value for that datatype.
Here's a very basic example: we want to have a blog, which has two types of pages: the homepage, and a page for each entry. URLs look like this:
- Blog post on January 1, 2010.
In other words, the homepage requires no extra parameters, and an entry page needs to know the date and the slug of the entry. This smells a lot like an algebraic data type; let's represent URLs as:
data BlogRoute = Homepage | Entry Day String
Along with this datatype, we need some way to parse an incoming URL to a datatype and some way to render a datatype back to a URL. In general, we would want some functions that basically follow these rules:
parse :: String -> Maybe BlogRoute render :: BlogRoute -> String -- parse . render = Just
To avoid the boilerplate of creating the datatypes and proper parse/render functions, Yesod uses web-routes-quasi.
Back to Hamlet
OK, so the majority of this post was really just speaking the praises of type-safe URLs, my appologies ;). Let's get back to the topic at hand: why does a hamlet have such a strange type signature? As a reminder:
[$hamlet|%h1 My Blog|] :: (a -> String) -> Html ()
Well, using our blog example, let's try to make that type signature a little bit clearer now:
[$hamlet|%a!href=@Homepage@ Go to blog homepage|] :: (BlogRoute -> String) -> Html ()
at-sign interpolation is used to include a type-safe URL; in this case, a value of type
BlogRoute. Suddenly, that mysterious
a in the type signature turned into
Does anyone notice that we've seen the
BlogRoute -> String type signature before? It's in fact the type of
render. This is no coincidence: a hamlet takes a URL rendering function. This is a huge boon for creating safe applications: during the entire construction of your template, you can deal with the URL datatypes directly. For the most part, every value for a
src attribute will be surrounded by at-signs. Finally, as you're ready to render your Hamlet template, you pass it your URL rendering function.
Note for Yesod users
For those of you using Hamlet in the context of Yesod, you are mostly sheltered from this whole discussion. Yesod provides the hamletToRepHtml and applyLayout functions which automatically apply the Hamlet template to the appropriate URL rendering function. When you want to embed one Hamlet template within another, you use carrot-interpolation (^otherTemplate^) and needn't be concerned about how it works. However, I think it is very important to have this knowledge in the back of your mind.
A plea to fellow Haskell web developers
I strongly believe that type-safe URLs are one of the most powerful features we Haskell web developers have at our disposal. I'd go so far as to say it should be one of our main advertisements of the power of Haskell.
Unfortunately, I think that the concept has had a lukewarm reception. Many people seem not to think the effort pays off. I think this is short-sighted; you've already admitted by using Haskell that you think spending a little extra time declaring types correctly is worth the payoff in the end from a more easily maintained codebase. Type-safe URLs are simply an extension of that decision.
I'll speak a bit from personal experience here: I have a project that's over 10,000 LOC and over 150 separate routes. (I would give more information, but I'm not allowed currently to discuss the project publicly.) At some point, the spec changed, and I suddenly needed to add an extra piece of information to the URL of almost every route. I was able to make the transition in about an hour: I simply changed the routes declaration, hit compile, and the compiler caught every single place that needed to be adjusted.
Perhaps you think that's an anomalous case. Then again, perhaps you've worked with clients and can empathize.