DEV Community

Brad
Brad

Posted on • Originally published at bradparker.com

Servant's type-level domain specific language

Originally posted here.

This post is about some reasonably advanced type-level features of The Glasgow Haskell Compiler and as such I assume some knowledge of Haskell. Despite this I've made an attempt to link to further resources on Haskell features as I introduce them. My hope is that even if everything here doesn't make perfect sense then at least some part of it might still be helpful.


I was shown Servant by a friend of mine not long after I'd started to learn Haskell. It was still new to me but one of the things that had really grabbed me about Haskell was its transparency. Without being very familiar with the language I found that I could tease out how some function or data structure worked just by poking around. Now I've spent a little time building stuff with Servant I'd like to see what can be learned by having a closer look at some of its functions and data structures. It's an interesting library, I think this'll be fun.

Servant has you define your API as a type. You're not expected to define a wholly new type, but rather combine existing types provided by the framework. These add up to a domain specific language, at the type level, for describing web APIs. This was quite a mental shift for me, that the type comes first, and drives the implementation. It's just so great that Servant's DSL is expressive enough to describe almost any API you might want to implement.

Our aim here will be to understand how Servant can take so many varied API descriptions and guide us to a corresponding implementation.

The example type

The example we'll use is close to the one used in Servant's introductory tutorial. We're going to describe an API which has two endpoints: GET /users which returns a JSON-encoded list of users and GET /users/:username which returns the user, again JSON-encoded, for a corresponding username.

We'll make use of type synonyms to group and give logical names to our API's sub-components.

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}

import Servant
  ( (:<|>)
  , (:>)
  , Capture
  , Get
  , JSON
  )

type UsersIndex =
  Get '[JSON] [User]

type UsersShow =
  Capture "username" String
    :> Get '[JSON] User

type UsersAPI =
  "users"
    :> (UsersIndex :<|> UsersShow)
Commit

Note for each code example I'll try to show only the bits of the finished module which are relevant to what's being currently discussed. Below some code samples you'll see a "commit" annotation which will link to a matching diff in the example repository.

This first code example already contains a few type-level features that likely look interesting. Let's take a closer look.

Type literals

Capture "username" String
        ~~~~~~~~~~

GHC supports both numeric and string-like type level literals. Of the greatest interest to us are the string-like type literals.

The first thing to note about these are that unlike most types we encounter in Haskell type-level string literals are not of kind Type but rather Symbol.

Note I'm using the extension NoStarIsType to replace * with Type when talking about kinds.

Another important thing to keep in mind is that each unique value is a different type. Or put another way, the type "foo" is distinct from the type "bar". When talking about these types as a whole it's helpful to step up to the kind layer and refer to them as Symbols. For example: "foo" and "bar" are different types, but they're both Symbols.

When working with Servant, Symbols are used to easily define things like static route segments, as well as named route and query parameters. Symbols can be brought down from the type level to strings at the value level. This means that Servant is able to extract data from API types for use at run time. More on this later.

Data type promotion

Get '[JSON] [User]
    ~~~~~~~

We don't have to be content with only strings and numbers moving up to the type level, by enabling the DataKinds language extension many other data types will become available for use up there too. Servant makes use of type level lists in some parts of its API.

The "tick" (') prefix is used to disambiguate type-level lists and the type of value-level lists. This is required in situations where GHC might need a hint to tell what is meant by, say:

[Int]

Is that the type for a list of Int, or is that a type level list with only one element, Int?

Similar to Symbol types like '[Int] and '[Bool] are not of kind Type. Those particular examples are of kind [Type]. It's worth noting, however, that the contents of a type level list need not always be of kind Type.

 > :kind '[Maybe]
'[Maybe] :: [Type -> Type]

If we ask GHCi for the kind of '[] we see something interesting.

 > :kind '[]
'[] :: [k]

This shows us that type level lists are kind polymorphic. That k is a kind variable, as with type variables these are introduced by an implicit forall.

'[] :: forall k. '[k]

Just as the type for value-level lists is parameterized over some arbitrary other type.

 > :type []
[] :: [a]

 > :type [1, 2, 3]
[1, 2, 3] :: Num a => [a]

 > :type [(+ 1), (+ 2), (+ 3)]
[(+ 1), (+ 2), (+ 3)] :: Num a => [a -> a]

The kind for type-level lists is parameterized over some arbitrary other kind.

 > :kind '[]
'[] :: [k]

 > :kind '[(), Bool, Ordering]
'[(), Bool, Ordering] :: [Type]

 > :kind '[Either (), Either Bool, Either Ordering]
'[Either (), Either Bool, Either Ordering] :: [Type -> Type]

Servant uses type level lists for a couple of purposes. In this post we'll see how they can help us specify the set of content types a given API can accept and return.

Type operators

type UsersAPI =
  "users"
    :> (UsersIndex :<|> UsersShow)
    ~~             ~~~~

By enabling the TypeOperators extension we can write infix type constructors in much same the way that we can value-level infix functions.

As with value-level infix operators, type operators have a precedence relative to other operators and can associate to the left or right. The syntax for defining these properties for a given type operator is the same as for value-level operators.

(f . g) a = f (g a)

infixr 9 .

type (f  g) a = f (g a)

infixr 9 

Putting it to use

As UsersAPI is the type for an API which serves users we'll need to define what a User is.

{-# LANGUAGE DeriveGeneric #-}

import Data.Time (Day)
import GHC.Generics (Generic)

data User = User
  { name :: String
  , age :: Int
  , email :: String
  , username :: String
  , registrationDate :: Day
  } deriving (Generic)
Commit

We're deriving a Generic instance for use later, it just helps to get it out-of-the-way now.

At the end of the day a runnable Haskell program is a value of type IO (). So how do we get from UsersAPI to one of those? The servant-server package provides us a few functions for turning types like UsersAPI into WAI Applications. The simplest of these is serve, so that's what we'll go with.

serve
  :: HasServer api '[]
  => Proxy api
  -> ServerT api Handler
  -> Application

The warp package provides a run function which gets us from an Application to a IO ().

run :: Port -> Application -> IO ()

In order for serve to return an Application we'll need to give it two things. The first is a Proxy of our UsersAPI type. If you've seen types like Map a or Set a before you might think that a Proxy a is some sort of container. Proxy is sort of a container, but rather a than for carrying around values it's for carrying around types.

data Proxy (t :: k) = Proxy

See that the only data constructor, also called Proxy, is nullary. At the value level it's empty, but at the type level it contains some t of kind k. Values of Proxy are made easier to construct with the help of the TypeApplications language extension. Using TypeApplications we can make explicit the types which are inferred and applied to our expressions.

 > :t Proxy
Proxy :: Proxy t
 > :t Proxy @Int
Proxy @Int :: Proxy Int
 > :t Proxy @UsersAPI
Proxy @UsersAPI :: Proxy UsersAPI

The Proxy @UsersAPI argument is needed by serve for slightly obscure reasons, but for our purposes it's enough to say that it's a value which carries the UsersAPI type around.

Producing a value for the second argument will be the topic of this post. We can use a typed hole for now, giving us enough for a skeleton main.

{-# LANGUAGE TypeApplications #-}

import Data.Proxy (Proxy(Proxy))
import Network.Wai (Application)
import Network.Wai.Handler.Warp (run)
import Servant (serve)

usersApp :: Application
usersApp = serve (Proxy @UsersAPI) _usersServer

main :: IO ()
main = run 8080 usersApp
Commit

Trying to compile what we have so far will result in two errors. The first is complaining about a missing instance.

• No instance for (ToJSON User) arising from a use of ‘serve’
• In the expression: serve (Proxy @Users) _usersServer
  In an equation for ‘usersApp’:
      usersApp = serve (Proxy @Users) _usersServer

The second tells us the type of the _usersServer value we've yet to define.

• Found hole:
    _usersServer
      :: Handler [User] :<|> ([Char] -> Handler User)
  Or perhaps ‘_usersServer’ is mis-spelled, or not in scope
• In the second argument of ‘serve’, namely ‘_usersServer’
  In the expression: serve (Proxy @Users) _usersServer
  In an equation for ‘usersApp’:
      usersApp = serve (Proxy @Users) _usersServer

We'll come back to why we're being asked about ToJSON instances but for now we'll just give Servant what it wants.

import Data.Aeson (ToJSON)

instance ToJSON User
Commit

A Server's type

We will try to understand where that ToJSON requirement came from but first we're going to focus on the typed hole. I think asking GHCi what the type of serve is when partially applied with a Proxy UsersAPI is instructive here.

 > :t serve (Proxy @UsersAPI)
serve (Proxy @UsersAPI)
  :: (Handler [User] :<|> ([Char] -> Handler User))
     -> Application

This is interesting. In the type of serve where previously there was a ServerT api Handler there is now a Handler [User] :<|> ([Char] -> Handler User). Where did it come from?

Recall that serve has the following type.

serve
  :: forall api
   . HasServer api '[]
  => Proxy api
  -> ServerT api Handler
  -> Application

There's a type variable api, which is constrained to be types with a HasServer instance. There's then two arguments, both referring to that constrained api type. We've been able to produce a value for the first using Proxy @UserAPI, a value for the second will take a little more doing.

I mean, what is ServerT? Why does it disappear when UsersAPI is substituted for api? It can't be a type constructor, type constructors don't just disappear. What does GHCi have to say about it?

 > :info ServerT
class HasServer (api :: k)
                (context :: [Type]) where
  type family ServerT (api :: k) (m :: Type -> Type) :: Type
  ...
        -- Defined in ‘Servant.Server.Internal’

ServerT is a type family. Type families look quite like type constructors. Like type constructors they accept types as arguments, unlike type constructors they're able to return different types depending on those arguments. It ends up looking something like a function which pattern matches on types.

ServerT is part of the HasServer type class, therefore it will be defined for any type which has a HasServer instance. It accepts the poly kinded api type HasServer is parameterized over as its first argument and some type constructor m of kind Type -> Type as its second. It then returns some type of kind Type.

GHCi allows us to evaluate type families, to see what the resulting type is at different type arguments.

 > :kind! ServerT Users Handler
ServerT UsersAPI Handler :: Type
= Handler [User] :<|> ([Char] -> Handler User)

Here we can see how UsersAPI becomes Handler [User] :<|> ([Char] -> Handler User) when substituted for api in the type of serve.

But that's not super satisfying to me. I feel like we're skipping a few steps. I'd like to see if we can find out what those steps are.

One of the great advantages of referentially transparent languages like Haskell is that if we want to see how an expression is evaluated we can do the evaluating ourselves, manually. We can substitute values for function parameters and continue evaluating the resulting expressions until we're only left with values. We'll attempt to apply this strategy to see what happens when UsersAPI is applied to ServerT.

Stepping through

When UsersAPI is substituted for api in the type of serve the ServerT type family is evaluated with it as its first argument and the Handler type as its second.

ServerT UsersAPI Handler

As ServerT is part of the HasServer type class there is a ServerT implementation for each HasServer instance. For us to figure out which ServerT to use we'll need to know which HasServer instance to grab it from. This means that not only are we going to be evaluating calls to ServerT ourselves, but we're also going to have to resolve type class instances too. How do we find the right HasServer instance for UsersAPI?

We can start by asking GHCi to tell us everything is knows about UsersAPI.

 > :info UsersAPI
type UsersAPI = "users" :> (UsersIndex :<|> UsersShow)
        -- Defined at src/Main.hs:75:1

GHCi knows that UsersAPI is a type synonym and helpfully shows us its definition.

We still don't have a HasServer instance, so we'll ask GHCi what it knows about the type that UsersAPI is a synonym for. Sadly, we can't pass the whole type to :info, we can only ask about things like type families or type constructors when unapplied. So we'll need to start with the outermost type constructor, (:>), and go from there.

The instance we're interested in will only show up if we import a couple of modules first.

 > import GHC.TypeLits
 > import Servant (HasServer)

We can now expect :info to tell us about the HasServer instances relevant to Symbols and (:>).

 > :info (:>)
type role (:>) phantom phantom
data (:>) (path :: k) a
        -- Defined in ‘Servant.API.Sub’
infixr 4 :>
instance (KnownSymbol path, HasServer api context) =>
         HasServer (path :> api) context
  -- Defined in ‘Servant.Server.Internal’
instance forall k l (arr :: k -> l) api (context :: [Type]).
         (TypeError ...) =>
         HasServer (arr :> api) context
  -- Defined in ‘Servant.Server.Internal’

Note that we're being shown two HasServer instances for (:>), and it actually has many more than these, but for now we're only interested in one of them.

instance
  ( KnownSymbol path
  , HasServer api context
  ) =>
    HasServer (path :> api) context

How do I know that this is the relevant instance?

KnownSymbol is a special type class definable only for types of kind Symbol. It allows us to "read" type-level Symbols into value-level Strings.

Seeing it in that instance head tells me that path is a Symbol, it couldn't have a KnownSymbol instance if it were anything else. UsersAPI has the Symbol "users" to the left of (:>) so it's a good match.

GHCi is able to tell us about type family instances for types, however in our case the output doesn't emphasize that ServerT instances belong to HasServer instances. To find the relevant ServerT it helps to look at HasServer's documentation and the linked source for the instance in question.

Now we know which ServerT will be applied.

type instance
  ServerT (path :> api) m =
    ServerT api m

We can take that, substitute "users" for path, UsersIndex :<|> UsersShow for api and Handler for m.

ServerT (UsersIndex :<|> UsersShow) Handler

Notice that ServerT is being called again in this substituted body. It recurses, having now peeled off the "users" :> part of the type. We have a new ServerT to find, this time for (:<|>).

We can again ask GHCi using :info to tell us what it knows about (:<|>). Fortunately there's only one HasServer instance and therefore only one ServerT.

type instance
  ServerT (a :<|> b) m =
    ServerT a m :<|> ServerT b m

Substituting UsersIndex for a, UsersShow for b and Handler for m gets us the following.

ServerT UsersIndex Handler :<|> ServerT UsersShow Handler

We're faced with two recursive calls to ServerT in this substituted body. For the next step we'll need to choose which branch to evaluate first. Let's begin on the left.

ServerT UsersIndex Handler

Which is the relevant ServerT for UsersIndex? Let's find out.

First UsersIndex is a synonym.

type UsersIndex =
  Get '[JSON] [User]

Asking GHCi what Get is will reveal that it too is a synonym.

 > :info Get
type Get =
  Servant.API.Verbs.Verb 'Network.HTTP.Types.Method.GET 200
  :: [Type] -> Type -> Type
        -- Defined in ‘Servant.API.Verbs’

Fortunately Verb is the end of the line.

 > :info Servant.API.Verbs.Verb
type role Servant.API.Verbs.Verb phantom phantom phantom phantom
data Servant.API.Verbs.Verb (method :: k1)
                            (statusCode :: Nat)
                            (contentTypes :: [Type])
                            a
        -- Defined in ‘Servant.API.Verbs’
instance [safe] forall k1 (method :: k1) (statusCode :: Nat) (contentTypes :: [Type]) a.
                Generic (Servant.API.Verbs.Verb method statusCode contentTypes a)
  -- Defined in ‘Servant.API.Verbs’
instance [overlappable] forall k1 (ctypes :: [Type]) a (method :: k1) (status :: Nat) (context :: [Type]).
                        (Servant.API.ContentTypes.AllCTRender ctypes a,
                         Servant.API.Verbs.ReflectMethod method, KnownNat status) =>
                        HasServer (Servant.API.Verbs.Verb method status ctypes a) context
  -- Defined in ‘Servant.Server.Internal’

There's our HasServer instance, and so we're able to find the corresponding ServerT.

type instance
  ServerT (Verb method status ctypes a) m =
    m a

Verbs are very general, we can make this look a bit simpler by inlining all the arguments that have been applied in the UsersShow type.

UsersShow
  =
Get '[JSON] [User]
  =
Verb GET 200 '[JSON] [User]

We now see that we can substitute [User] for a. Because we're tracing the evaluation of ServerT as it's used in serve we'll always be substituting Handler for m. Making those two substitutions in the body of the type family instance above will result in the following.

Handler [User]

That's the left branch of (:<|>) done.

Handler [User] :<|> ServerT UsersIndex Handler

On to the right.

ServerT UsersShow Handler

What was UsersShow a synonym for?

type UsersShow =
  Capture "username" String
    :> Get '[JSON] User

It's outermost type constructor is (:>), which we've seen before. Despite this we haven't yet seen the ServerT that we'll need to evaluate this next step.

Remember that the instance we last saw required that the first argument to (:>) be of kind Symbol. The first argument to (:>) in UsersShow, however, isn't. It's a bad match, we'll have to find another instance.

Let's try asking about Capture.

 > :info Capture
type Capture = Servant.API.Capture.Capture' '[] :: Symbol -> Type -> Type
        -- Defined in ‘Servant.API.Capture’

We're told it's a synonym for Capture'.

 > import Servant.API.Capture (Capture')
 > :info Capture'
type role Capture' phantom phantom phantom
data Capture' (mods :: [Type]) (sym :: Symbol) a
        -- Defined in ‘Servant.API.Capture’
instance (KnownSymbol capture,
          Web.Internal.HttpApiData.FromHttpApiData a,
          HasServer api context) =>
         HasServer (Capture' mods capture a :> api) context
  -- Defined in ‘Servant.Server.Internal’

Capture' has only one HasServer instance, and it's defined only for Capture's which appear on the left-hand side of (:>). This looks like a good match.

The ServerT for this instance is, I think, the most interesting we've seen.

type instance
  ServerT (Capture' mods capture a :> api) m =
    a -> ServerT api m

Substituting '[] for mods, "username" for capture, String for a, Get '[JSON] User for api and Handler for m gives us a function.

String -> ServerT (Get '[JSON] User) Handler

This is magical. A Capture is transformed into a function which accepts the path parameter it represents.

We're nearly finished evaluating, we have one more call to ServerT.

ServerT (Get '[JSON] User) Handler

Fortunately we already know the ServerT to use here.

type instance
  ServerT (Verb method status ctypes a) m =
    m a

So let's apply it.

Handler User

And we're finished evaluating ServerT UsersShow Hander.

String -> Handler User

Which means we're finished evaluating ServerT UsersIndex Handler :<|> ServerT UsersShow Handler.

Which means we're finished evaluating ServerT UsersAPI Handler.

Handler [User] :<|> (String -> Handler User)

Here's all of those steps together.

ServerT UsersAPI Handler
ServerT ("users" :> (UsersIndex :<|> UsersShow)) Handler
ServerT (UsersIndex :<|> UsersShow) Handler
ServerT UsersIndex Handler :<|> ServerT UsersShow Handler
ServerT (Get '[JSON] [User]) Handler :<|> ServerT UsersShow Handler
ServerT (Verb GET 200 '[JSON] [User]) Handler :<|> ServerT UsersShow Handler
Handler [User] :<|> ServerT UsersShow Handler
Handler [User] :<|> ServerT (Capture "username" String :> Get '[JSON] User) Handler
Handler [User] :<|> (String -> ServerT (Get '[JSON] User)) Handler
Handler [User] :<|> (String -> ServerT (Verb GET 200 '[JSON] User)) Handler
Handler [User] :<|> (String -> Handler User)

So this is how Servant goes about transforming the UsersAPI type into the type for a server. We first declared a reasonable looking shape for our API as a type and now Servant is letting us know how we can implement a server for it.

Before we do that, however, we had another type error we were going to look into.

Content types

Why did we need to define a ToJSON instance for User? Where did that constraint come from?

We mention JSON twice in the type of UsersAPI, in both instances it's as an argument to the Get type constructor. Recall from above that Get is an alias for Verb, recall also that the only constraint on serve is that the provided api type has a HasServer instance. Is there anything interesting about the HasServer instance for Verb?

instance
  forall
    k1
    (ctypes :: [Type])
    a
    (method :: k1)
    (status :: Nat)
    (context :: [Type]).
  ( AllCTRender ctypes a
  , ReflectMethod method
  , KnownNat status
  ) =>
    HasServer (Verb method status ctypes a) context

The HasServer instance for Verb constrains the a type parameter to be an instance of a type class called AllCTRender. If we go looking for instances of this type class we're faced with something that might seem a little strange.

instance
  (TypeError ...) =>
    AllCTRender '[] ()

instance
  ( Accept ct
  , AllMime cts
  , AllMimeRender (ct : cts) a
  ) =>
    AllCTRender (ct : cts) a

It has two instances. One instance which will throw a custom type error when applied to an empty list and (), and one that doesn't refer to any concrete types, strange stuff.

Notice that type level lists can be pattern-matched and de-structured quite like value level lists, here in the constraints of this type class instance. This means that were able to iterate over type level lists in much the same way that we do for those at the value level. The iteration splits off into three more type classes: Accept, AllMime and AllMimeRender.

AllMimeRender has two instances, and with these this really starts to look like value level list iteration.

instance
  ( MimeRender ctyp a
  ) =>
    AllMimeRender '[ctyp] a

instance
  ( MimeRender ctyp a
  , AllMimeRender (ctyp' : ctyps) a
  ) =>
    AllMimeRender (ctyp : ctyp' : ctyps) a

We have a base case where there's only one element in the list. The other instance asserts that the head of the list has an instance of MimeRender and the (non-empty) tail has one for AllMimeRender, that is: it recurses.

Our API only speaks JSON, represented by its content-types list being '[JSON], so the first instance of AllMimeRender is used. This means that there needs to be an instance of MimeRender for JSON. By looking for that instance we see what we've been looking for.

instance ToJSON a => MimeRender JSON a

Here's how it all goes.

  • In order for there to be an instance of HasServer for Verb GET 200 '[JSON] User there has to be an instance of AllCTRender for '[JSON] and User
  • In order for there to be an instance of AllCTRender for '[JSON] and User there has to be an instance of AllMimeRender for '[JSON] and User
  • In order for there to be an instance of AllMimeRender for '[JSON] and User there has to be an instance of MimeRender for JSON and User
  • In order for there to be an instance of MimeRender for JSON and User there has to be an instance of Aeson's ToJSON for User

So that's why when we tried to apply UsersAPI to serve we needed to define a ToJSON instance for User.

Implementing a server for our type

Now we know how the typed hole _usersServer ends up with the type it does.

Handler [User] :<|> (String -> Handler User)

Let's go about creating a value of this type. We might start from the outside, in much the same order as we traced the evaluation of ServerT. This means first figuring out how to construct a value of type a :<|> b. Using GHCi we're able to view the definition of this type.

data (:<|>) a b = a :<|> b

It's pretty much a pair.

data (,) a b = (a, b)

It has only one data constructor, which happens to share the type's name. There's only one thing we can do, provide that constructor two values. Now, we don't actually have those values yet, so let's use typed holes for now.

usersServer :: Server UsersAPI
usersServer = _usersIndex :<|> _usersShow
Commit

Applying this to serve will let us know if we're on the right track.

  usersApp :: Application
- usersApp = serve (Proxy @UsersAPI) _usersServer
+ usersApp = serve (Proxy @UsersAPI) usersServer

Trying to compile should give us something like the following.

src/Main.hs:86:15: error:
    • Found hole: _usersIndex :: Handler [User]
      Or perhaps ‘_usersIndex’ is mis-spelled, or not in scope
    • In the first argument of ‘(:<|>)’, namely ‘_usersIndex’
      In the expression: _usersIndex :<|> _usersShow
      In an equation for ‘usersServer’:
          usersServer = _usersIndex :<|> _usersShow
    • Relevant bindings include
        usersServer :: Server UsersAPI (bound at src/Main.hs:86:1)
   |
86 | usersServer = _usersIndex :<|> _usersShow
   |               ^^^^^^^^^^^

src/Main.hs:86:32: error:
    • Found hole: _usersShow :: [Char] -> Handler User
      Or perhaps ‘_usersShow’ is mis-spelled, or not in scope
    • In the second argument of ‘(:<|>)’, namely ‘_usersShow’
      In the expression: _usersIndex :<|> _usersShow
      In an equation for ‘usersServer’:
          usersServer = _usersIndex :<|> _usersShow
    • Relevant bindings include
        usersServer :: Server UsersAPI (bound at src/Main.hs:86:1)
   |
86 | usersServer = _usersIndex :<|> _usersShow
   |                                ^^^^^^^^^^

Amongst that we're being told that we have two values we need to conjure up.

usersIndex :: Handler [User]
usersIndex = _

usersShow :: String -> Handler User
usersShow _uname = _
Commit

We'll start with usersIndex, which is a value of type Handler [User].

For the sake of this example our collection of users will be some static, sample data. I might do another post on my experience of using Beam in a Servant application, but for now let's keep it simple.

users :: [User]
users =
  [ User
    { name             = "Isaac Newton"
    , age              = 372
    , email            = "isaac@newton.co.uk"
    , username         = "isaac"
    , registrationDate = fromGregorian 1683 3 1
    }
  , User
    { name             = "Albert Einstein"
    , age              = 136
    , email            = "ae@mc2.org"
    , username         = "albert"
    , registrationDate = fromGregorian 1905 12 1
    }
  ]
Commit

Now that we have a value of [User] we need a function which can wrap it in a Handler. Looking at the documentation for Handler we see that it has an Applicative instance which will provide us just what we need.

 > :type pure @Handler
pure @Handler :: a -> Handler a

So there we have it.

usersIndex :: Handler [User]
usersIndex = pure users
Commit

For UsersShow we'll have a little more work to do. We're supplied the user name of the user we'd like returned, we should use it to look for that user in users. The function we'll need for finding elements of lists is find.

find :: Foldable t => (a -> Bool) -> t a -> Maybe a

Or more specifically.

 > :t find @[] @User
find @[] @User :: (User -> Bool) -> [User] -> Maybe User

We'll need a function User -> Bool and in our case the Bool should indicate whether a provided String matches a given Users username.

matchesUsername :: String -> User -> Bool
matchesUsername uname = (uname ==) . username

We're nearly there.

 > :t \uname -> find @[] (matchesUsername uname)
\uname -> find @[] (matchesUsername uname)
  :: String -> [User] -> Maybe User

 > :t \uname -> find @[] (matchesUsername uname) users
\uname -> find @[] (matchesUsername uname) users
  :: String -> Maybe User

The final step is to turn a Maybe User into a Handler User. For the Just case we have a User and are able to use pure @Handler to wrap it in a Handler, but for the Nothing case, what should we do?

usersShow :: String -> Handler User
usersShow uname =
  case find (matchesUsername uname) users of
    Nothing   -> _
    Just user -> pure user
Commit

Ordinarily when you ask a web server for a resource it can't find you get a 404 response back. How can we produce a value of Handler User that results in a 404?

By looking at the docs for Handler again we can see that it has a MonadError instance, this suggests that we can use throwError when we need to return something but can't return a User.

By looking at Handler's instance for MonadError we see that it's defined for a ServantErr type. So in our case throwError has the following type.

 > :t throwError @ServantErr @Handler
throwError @ServantErr @Handler :: ServantErr -> Handler a

Now it helps to look at the documentation for ServantErr. There we see quite a few values of the type, including a quite relevant-looking err404.

usersShow :: String -> Handler User
usersShow uname =
  case find (matchesUsername uname) users of
    Nothing   -> throwError err404
    Just user -> pure user
Commit

We've now defined everything we need and should have a runnable server.

Start it up.

$ runhaskell src/Main.hs

And try it out.

$ curl -sD /dev/stderr http://localhost:8080/users | jq .
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Sat, 21 Sep 2019 05:26:33 GMT
Server: Warp/3.2.28
Content-Type: application/json;charset=utf-8

[
  {
    "email": "isaac@newton.co.uk",
    "registrationDate": "1683-03-01",
    "age": 372,
    "username": "isaac",
    "name": "Isaac Newton"
  },
  {
    "email": "ae@mc2.org",
    "registrationDate": "1905-12-01",
    "age": 136,
    "username": "albert",
    "name": "Albert Einstein"
  }
]

$ curl -sD /dev/stderr http://localhost:8080/users/albert | jq .
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Sat, 21 Sep 2019 05:25:09 GMT
Server: Warp/3.2.28
Content-Type: application/json;charset=utf-8

{
  "email": "ae@mc2.org",
  "registrationDate": "1905-12-01",
  "age": 136,
  "username": "albert",
  "name": "Albert Einstein"
}

$ curl -sD /dev/stderr http://localhost:8080/users/unknown | jq .
HTTP/1.1 404 Not Found
Transfer-Encoding: chunked
Date: Sat, 21 Sep 2019 05:26:00 GMT
Server: Warp/3.2.28

What's it all for?

My hope when planning this post was that I'd become a little more familiar with the type level programming features of GHC Haskell. I wasn't sure which features or to what extent. Having finished I'd say that I've started to understand this topic. At the very least I've spent a bit of time becoming more familiar with a library that makes great use of GHC's type level features.

The DSL provided by Servant allows us to construct types which specify an API contract. With it we were able to specify static route segments and named route parameters using Symbols. We could associate those routes with HTTP verbs which could accept and return many content-types using type level lists. Combining these components was made easy with infix type constructors.

The way Servant has us think "specification first" is very appealing to me, and the more time I spend with Haskell the more this method of designing and implementing software just feels right. The type level feels much more declarative: you don't talk as much about what you want to happen, instead you talk more about what things you would like to exist. Then it's up to you and the compiler to figure out how that might be possible.

There's another, more practical, benefit to this in Servant's case however. We've seen that specifications can be turned into the types of servers which implement them, however we didn't explore how we can also use them to automatically produce clients, documentation, and even property tests.

I imagine investigating those packages would be good for even more type level learnings.

Top comments (0)