In the previous example we used template haskell to automatically derive a mapping between the url type and the url string. This is very convenient early in the development process when the routes are changing a lot. But the resulting urls are not very attractive. One solution is to write the mappings from the url type to the url string by hand.
One approach would be to write one function to show the urls, and another function that uses parsec to parse the urls. But having to say them same thing twice is really annoying and error prone. What we really want is a way to write the mapping once, and automatically exact a parser and printer from the specification.
Fortunately, Sjoerd Visscher and Martijn van Steenbergen figured out exactly how to do that and published a proof of concept library know as Zwaluw. With permission, I have refactored their original library into two separate libraries: boomerang and web-routes-boomerang.
The technique behind Zwaluw and Boomerang is very cool and interesting to understand. But in this tutorial we will skip the theory and get right to practice.
In order to run this demo you will need to install web-routes, web-routes-boomerang and web-routes-happstack from hackage.
In this example, we will simply modify the previous example to show how easy it is use a different method for defining the mapping between the url type and the url string. We will also add a few new routes to demonstrate some features of using boomerang.
The first thing to notice is that we hide id and (.) from the Prelude and import the versions from Control.Category instead.
Next we have our Sitemap types again. The Sitemap is similar to the previous example, except it also includes UserOverview and UserDetail.
Next we call derivePrinterParsers:
That will create new combinators corresponding to the constructors
for Sitemap. They will be named, rHome, rArticle, rUserOverview, and rUserDetail.
Now we can specify how the Sitemap type is mapped to a url string and back:
The mapping looks like this:
| url | type | |
|---|---|---|
| / | <=> | Home |
| /article/int | <=> | Article int |
| /users | <=> | UserOverview |
| /users/int-string | <=> | UserDetail int string |
By examining the mapping table and comparing it to the code, you should be able to get an intuitive feel for how boomerang works. The key boomerang features we see are:
<><> is the choice operator. It chooses between the various paths.litlit matches on a string literal. If you enabled OverloadedStrings then you do not need to explicitly use the lit function. For example, you could just write, int . "-" . anyString... is used to combine elements together.</>lit, int, anyString, operate on a single path segment. </> matches on the / between path segments.xmaphxmaph is a bit like fmap, except instead of only needing a -> b it also needs the other direction, b -> Maybe a.
xmaph to convert int :: Router Int into articleId :: Router ArticleId.
/users comes before /users/int-string. Unlike parsec, the order of the parsers (usually) does not matter. We also do not have to use try to allow for backtracking. boomerang will find all valid parses and pick the best one. Here, that means the parser that consumed all the available input.The sitemap function looks like an ordinary parser. But, what makes it is exciting is that it also defines the pretty-printer at the same time.
Next we need a function that maps a route to the handlers. This is the same exact function we used in the previous example extended with the additional routes:
Next, we have the handler functions. These are also exactly the same as the previous example, plus the new routes:
Creating the Site type is similar to the previous example. We still use runRouteT to unwrap the RouteT layer. But now we use boomerangSite to convert the route function into a Site:
The route function is essentially the same in this example and the previous example. The difference is in how we generate the formatPathSegments and parsePathSegments functions. In the previous example, we used mkSitePI, which leveraged the PathInfo instances. Here we use boomerangSite which uses the sitemap mapping we defined above.
The practical result is that you can start by using derivePathInfo so you don't have to think about how the urls will look. Later, once the routes have settled down, you can then easily switch to using boomerang to create your route mapping.
Next we use implSite to embed the Site into a normal Happstack route:
[Source code for the app is here.]