As a co-author, I’m obviously happy if the paper is being read, but it’s also 12 pages long in two-column ACM style. And while it explains the implementation, it does not necessarily make it easier to start playing with the code yourself, because it only shows excerpts, and code snippets in a paper are not easily runnable.
At the same time, whenever I want to demonstrate a new concept in the Servant context, or play with new ideas, I find myself not impementing it in the main Servant code base, but rather to create a small library that is “like Servant”, built on the same principles, but much simpler, so that I have less work to do and can evaluate the ideas more quickly. I’ve talked to some other contributors, and at least some of them are doing the same. So I thought it might be useful to develop and present the code of “TinyServant”, which is not exactly tiny, but still small compared to the full Servant code base, strips away a lot of duplication and unessential extras, but is still complete enough so that we can observe how Servant works. Obviously, this still won’t explain everything that one might want to know about the implementation of Servant, but I hope that it will serve as a useful ingredient in that process.
This blog post is a somewhat revised and expanded version of my Stack Overflow answer.
This is not a general tutorial on Servant and using Servant. For learning how to use Servant, the official Servant tutorial or the general documentation section of the Servant home page are better starting points.
The full code that is discussed in this post is 81 lines of Haskell and available separately.
I’m going to show the following things:
How to define the web API specification language that Servant offers. We are going to define as few constructs as possible: we are not going to worry about content types (just plain text), we are not going to worry about different HTTP methods (just GET), and the only special thing we can do in routes will be that we can capture components of the path. Still, this is enough to show all relevant ideas of the Servant implementation.
How to use TinyServant on an example. We are going to take the very first example of the Servant homepage and adapt it for our examples.
To start, here are the language extensions we’ll need:
The first three are needed for the definition of the type-level DSL itself. The DSL makes use of type-level strings (
DataKinds) and also uses kind polymorphism (
PolyKinds). The use of the type-level infix operators such as
:> requires the
The second three are needed for the definition of the interpretation. For this, we need type-level functions (
TypeFamilies), some type class programming which will require
FlexibleInstances, and some type annotations to guide the type checker which require
Purely for documentation purposes, we also use
Here’s our module header:
The import of
Data.Time is just for our example.
The first ingredient is to define the datatypes that are being used for the API specifications.
As I’ve said before, we define only four constructs in our simplified language:
Get arepresents an endpoint of type
*). In comparison with full Servant, we ignore content types here. We need the datatype only for the API specifications. There are no directly corresponding values, and hence there is no constructor for
a :<|> b, we represent the choice between two routes. Again, we wouldn’t need a constructor, but it will turn out to be useful later when we define handlers.
item :> rest, we represent nested routes, where
itemis the first path component and
restare the remaining components. In our simplified DSL, there are just two possibilities for
item: a type-level string, or a
Capture. Because type-level strings are of kind
Symbol, but a
Capture, defined below is of kind
*, we make the first argument of
:>kind-polymorphic, so that both options are accepted by the Haskell kind system. So in particular, we will be able to write both
and it will be well-kinded.
Capture arepresents a route component that is captured, parsed and then exposed to the handler as a parameter of type
a. In full Servant,
Capturehas an additional string as a parameter that is used for documentation generation. We omit the string here.
We can now write down a version of the API specification from the Servant home page, adapted to our simplified DSL, and replacing the datatypes used there by actual datatypes that occur in the
Interpretation as server
The most interesting aspect is of course what we can do with the API. Servant defines several interpretations, but they all follow a similar pattern. We’ll define only one here, which is inspired by the interpretation as a web server.
In Servant, the
serve function has the following type:
It takes a proxy for the API type (we’ll get back to that in a moment), and a handler matching the API type (of type
Server layout) to an
Application type comes from the excellent WAI library that Servant uses as its default backend.
Even though WAI is very simple, it is too complicated for the purposes of this post, so we’ll assume a “simulated server” of type
This server is supposed to receive a request that is just a sequence of path components (
[String]). We do not care about request methods or the request body or anything like that. And the response it just a message of type
String. We ignore status codes, headers and anything else. The underlying idea is still the same though than that of the
Application type used in the actual Servant implementation.
serve function has type
HasServer class, which we’ll define below, has instances for all the different constructs of the type-level DSL and therefore encodes what it means for a Haskell type
layout to be interpretable as an API type of a server.
Proxy type is defined as follows: It’s defined as
Its only purpose is to help the GHC type checker. By passing an explicitly typed proxy such as
Proxy :: Proxy MyAPI to
serve, we can explicitly instantiate the
serve function to a particular API type. Without the
Proxy, the only occurrences of the
layout parameter would be in the
HasServer class constraint and as an argument of
Server, which is a type family. GHC is not clever enough to infer the desired value of
layout from these occurrences.
Server argument is the handler for the
API. As just stated,
Server itself is a type family (i.e., a type-level function), and computes from the API type the type that the handler(s) must have. This is one core ingredient of what makes Servant work correctly.
From these inputs, we then compute the output function of type
[String] -> IO String as explained above.
Server type family
Server as a type family first. (Again, this is somewhat simplified compared to
Servant, which defines a monad transformer type family called
ServerT as part of the
HasServer class and then a top-level type synonym
Server in terms of
The handler for a
Get a endpoint is simply an
IO action producing an
a. (Once again, in the full Servant code, we have slightly more options, such as producing an error with a choice of status codes.)
The handler for
a :<|> b is a pair of handlers, so we could just define
But with this definition, nested occurrences of
:<|> in the API would lead to nested pairs of handlers, so we’d have to write code like
which looks a bit ugly. Instead, we’re going to make
:<|> equivalent to Haskell’s pair type, but with an infix constructor called
:<|>, so that we can write
for a nested pair. The actual definition of
:<|> is then
It remains to explain how each of the path components is handled.
Literal strings in the routes do not affect the type of the handler:
A capture, however, means that the handler expects an additional argument of the type being captured:
Computing the handler type of the example API
If we expand
Server MyAPI, we obtain
Server MyAPI ~ Server ( "date" :> Get Day :<|> "time" :> Capture TimeZone :> Get ZonedTime ) ~ Server ("date" :> Get Day) :<|> Server ("time" :> Capture TimeZone :> Get ZonedTime) ~ Server (Get Day) :<|> Server ("time" :> Capture TimeZone :> Get ZonedTime) ~ IO Day :<|> Server ("time" :> Capture TimeZone :> Get ZonedTime) ~ IO Day :<|> Server (Capture TimeZone :> Get ZonedTime) ~ IO Day :<|> TimeZone -> Server (Get ZonedTime) ~ IO Day :<|> TimeZone -> IO ZonedTime
~ is GHC’s syntax for type equality.
:<|> as defined is equivalent to a pair. So as intended, the server for our API requires a pair of handlers, one that provides a date of type
Day, and one that, given a time zone, provides a time (of type
ZonedTime). We can define the handler(s) right now:
We still have to implement the
HasServer class, which looks as follows:
The task of the function
route is almost like
serve. Internally, we have to dispatch an incoming request to the right router. In the case of
:<|>, this means we have to make a choice between two handlers. How do we make this choice? A simple option is to allow
route to fail, by returning a
Maybe. Then in a choice we can just try the first option, and if it returns
Nothing, try the second. (Again, full Servant is somewhat more sophisticated here, and version 0.5 will have a much improved routing strategy, which probably at some point in the future deserves to be the topic of its own blog post.)
Once we have
route defined, we can easily define
serve in terms of
If none of the routes match, we fail with a (simulated) 404. Otherwise, we return the result.
Get endpoint, we defined
so the handler is an IO action producing an
a, which we have to turn into a
String. We use
show for this purpose. In the actual Servant implementation, this conversion is handled by the content types machinery, and will typically involve encoding to JSON or HTML.
Since we’re matching an endpoint only, the require the request to be empty at this point. If it isn’t, this route does not match and we return
Let’s look at choice next:
Here, we get a pair of handlers, and we use
Maybe to try both, preferring the first if it matches.
What happens for a literal string?
The handler for
s :> r is of the same type as the handler for
r. We require the request to be non-empty and the first component to match the value-level counterpart of the type-level string. We obtain the value-level string corresponding to the type-level string literal by applying
symbolVal. For this, we need a
KnownSymbol constraint on the type-level string literal, but all concrete literals in GHC are automatically an instance of
The final case is for captures:
In this case, we can assume that our handler is actually a function that expects an
a. We require the first component of the request to be parseable as an
a. Here, we use the
Read class, whereas in Servant, we use a special-purpose class called
FromHttpApiData in version 0.5). If reading fails, we consider the request not to match. Otherwise, we can feed it to the handler and continue.
Now we’re done.
We can confirm that everything works in GHCi:
GHCi> serve (Proxy :: Proxy MyAPI) handleMyAPI ["time", "CET"] "2015-11-01 20:25:04.594003 CET" GHCi> serve (Proxy :: Proxy MyAPI) handleMyAPI ["time", "12"] *** Exception: user error (404) GHCi> serve (Proxy :: Proxy MyAPI) handleMyAPI ["date"] "2015-11-01" GHCi> serve (Proxy :: Proxy MyAPI) handleMyAPI  *** Exception: user error (404)
We now have a system that we can play with an extend and modify easily. We can for example extend the specification language by a new construct and see what we have to change. We can also make the simulation more faithful (e.g. include request bodies or query parameters). Or we can define a completely different interpretation (e.g. as a client) by following the same scheme.