Type safe routing means different things to different people. So let’s start by clarifying the intended meaning in this post. There are 2 widely used definitions:
The first refers to the case where the type system is used to maintain hyper links in your web app so that there are no dangling links. This static checking also extends to the parameters accepted by the hyper links.
The second definition is processing URL’s in a way that assigns types to the values extracted.
The first definition is much stronger and is mostly useful for complete web apps. The second definition pertains to API’s exposed over HTTP and will be the subject of this blog post. I am not an expert on this subject and this post only documents my first steps as to how to solve this problem. The end solution that I propose is not quite satisfying but is elegant in its own right. Let’s begin.
Let’s take a sample opium handler:
1 2 3 4 5 6
There is something very unsatisfying in the code above. We are attaching a
function to a URL that contains 2 parameters
y. We know in the body
of the function that the parameters are always there (otherwise our handler
would not be executed) and yet we don’t express this deduction statically.
Can we do better? Yes, and I’ll show you how. Let’s start by pretending that a solution exists:
1 2 3 4 5
This is much better. The function that we are binding to the route is now
typed. The combinators used to specify the routes should be familiar, but a
brief explanation is in order.
s x will consume a portion of the url defined
</> concatenates 2 routes sequentially.
str specifies a string
parameter that ends with a
The advantages of this approach include:
We don’t have to extract the parameters manually out of the request or even name the parameters. Although, we can imagine adding support for that if it proves useful.
The compiler will warn us if we haven’t used an argument.
We can now specify the types of the parameters we accept. We can imagine using
intto specify a numerical parameter. In fact, we can probably think of a host of combinators to add various functionality.
The advantages are obvious, but how can we implement it?
Aspiring OCaml hackers might want give this a problem a crack themselves. To specify the problem a little better, and perhaps give a hint to the solution this is interface which we will be implementing:
1 2 3 4 5 6 7 8 9 10 11
If you have a hard problem related to static typing, a good rule of thumb is to check if Oleg has already solved it. Indeed, the problem above falls under that category but under a different context. Typed Formatting attempts to define an EDSL for specifying a type safe printf/scanf. Our problem of type safe routes is just a subset of that since we only need scanf. Oleg’s solution in Haskell involves GADT’s, which have almost have worn off the novelty for me in OCaml.
We will also need a generic parser type (
'a Parser.t is a parser that yields
a value of type
'a). A simple monadic parser will do. Since it’s not
particularly interesting, I will link my own implementation in the end. Here’s
the signature for the parser (Although I don’t think we will need all of these
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Finally, here’s the type representing our DSL:
1 2 3 4
Enumerating the constructors:
Try_parserepresents a parser whose result we discard. Hence we use
(). It will be useful to implement
Parserepresents a parser whose result we retain. It will be useful to implement typed parameters
Concatwill be used to sequence routes. It will be useful to implement
The key to the whole solution is one simple function interpreting our DSL:
1 2 3 4 5 6 7 8 9
This function takes a typed route and a function corresponding to a handler and returns a parser that returns the result of our handler on success.
We just need a bit more massaging to fit our proposed interface:
1 2 3 4 5 6 7 8 9
The rest is a small matter of programming the combinators:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
A little test using oUnit:
1 2 3 4 5 6 7 8 9
I have a prototype of Opium using these routes but it’s not for public consumption. I’d like to totally revamp routes from a scalability standpoint first before I improve the API for routing. There are also many other approaches to this problem (mostly from the land of Haskell). Here’s some great resources:
Happstack - The “pioneer” of type safe routing
The most recent and promising servant. However, it covers a whole lot more than just routing.
Spock is a tiny Haskell web framework that is most similar to my approach, although it uses type families under the hood.
At the end, I’m not sure which approach is the best for OCaml so I’m not ready to commit to anything in Opium yet. However, one thing is for certain: I definitely intend to the have the current untyped approach as an option.
Here’s a gist of a working version of my library. You’re welcome to fork and experiment, please let me know if you find any bugs.
One last thing. My implementation above can be called an “initial encoding” of the EDSL. You can actually implement the EDSL as a “final encoding” without GADT’s. I don’t see the advantage in either approach so I went with the initial encoding. I reached for it first and it gives me an excuse to play with GADT’s. A solution that uses a final encoding will be left as an exercise to the reader. (Who else has fond memories of this phrase from back in college?)