Really type-safe URLs

May 13, 2010

GravatarBy Michael Snoyman

It's my son's second birthday. Yay!

Anyway, on to the real stuff. Every once in a while, you get those "eureka" moments, where something that's been bothering you finally clicks. In this case, it was killing two birds with one stone. The stone is a small changes to web-routes-quasi which makes the generated URL datatypes even more typesafe. But before we talk about the stone anymore, let's discuss those birds.

Bird #1: Useless data

I've been collecting some data in web-routes-quasi which I've been completely ignoring. For those of you not familiar, web-routes-quasi defines a nice, simple, quasi-quoted syntax for declaring URL dispatch. It converts something like this:

/            RootR  GET
/entry/$slug EntryR GET
into something like this:
data BlogRoutes = RootR | EntryR String
It also generates a parse function, dispatch function, and notably here a dispatch function which requires the user to define the following two functions:
getRootR :: Handler
getEntryR :: String -> Handler

This is all very nice, but there's one thing bothering me: in the second line of the quasi-quoted data, what the hell does the word slug do? It's just sitting there, having no influence on the code! Well, it does give the programmer an idea of the purpose of that piece of the URL, but the computer completely ignores it.

Maybe this isn't the worst thing in the world, but it was always lurking in the back of my mind, waiting...

Bird #2: Not completely type-safe URLs

We've come a long way from the horrors which is PHP. Jeremy Shaw's web-routes package encourages us to never, ever use direct string concatenation to generate URLs. Instead of: "/foo/" ++ bar ++ "/" ++ show baz ++ "/" we can write a beautiful render $ Foo bar baz, and the compiler checks all of our errors for us.

Wait... all of our errors? Maybe not. Let's say I'm writing an application to calculate your BMI. I want to construct URLs with the datatype data BmiRoute = BmiRoute Integer Integer, where the first integer is your height in centimeters and the second is your weight in kilograms.

I'm sure you can see where this is going: One day I make a little mistake and type in BmiRoute weight height, and suddenly the entire population is obese.

Damn you web-routes! You failed me! I exclaim in fury as 2000 people e-mail me to let me know how fat they are. On the bright side, I end up getting a very nice check in the mail from Weight Watchers.

I'm sure most Haskellers already know the solution to this little predicament: I defined my types badly. Instead, I should have done:

newtype Height = Height Integer
newtype Weight = Weight Integer
data BmiRoute = BmiRoute Height Weight
Then, when I stupidly type in BmiRoute (Weight weight) (Height height), the compiler automatically catches the mistake.

That may have been a silly example, but imagine instead we were dealing with database keys; do you want to get your user_id confused with your email_id? I didn't think so.

The problem is, that web-routes-quasi only handles Integers, Strings and [String]s. So what's a man to do?

The stone

Many of you have probably already figured out what I'm about to say, but I'll spell it out anyway, cause I'm just too excited to keep quiet (it is my son's birthday after all). That useless syntax in the quasi-quoted routes will now define the datatype of the parameter. To use the BMI example, I could write /bmi/#Height/#Weight Bmi GET, and the resulting constructor will be Bmi Height Weight.

The pound sign here will still mean only to accept integers, but now the fromInteger method will be called to convert the plain Integer into a Weight and Height. For strings and slurps, you use the ToString and IsSlurp typeclasses, respectively. ToString is a subclass on IsString, adding the toString function, and IsSlurp defines the toSlurp and fromSlurp functions.


I e-mailed the web-devel list just yesterday saying I thought Yesod 0.2 was feature complete; well, this change is definitely going into Yesod 0.2. The change in web-routes-quasi is done, and will be released as version 0.2 shortly (I'll do a little more testing first).


comments powered by Disqus