Servant's type-level domain specific language
Posted on 05 October, 2019
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)
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 Symbol
s. For example: "foo"
and "bar"
are different types, but they're both Symbol
s.
When working with Servant, Symbol
s are used to easily define things like static route segments, as well as named route and query parameters. Symbol
s 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.
. g) a = f (g a)
(f
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) }
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 Application
s. 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
= serve (Proxy @UsersAPI) _usersServer
usersApp
main :: IO ()
= run 8080 usersApp main
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
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 Symbol
s 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 Symbol
s into value-level String
s.
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
Verb
s 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 =
-> ServerT api m a
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
k1ctypes :: [Type])
(
amethod :: 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
forVerb GET 200 '[JSON] User
there has to be an instance ofAllCTRender
for'[JSON]
andUser
- In order for there to be an instance of
AllCTRender
for'[JSON]
andUser
there has to be an instance ofAllMimeRender
for'[JSON]
andUser
- In order for there to be an instance of
AllMimeRender
for'[JSON]
andUser
there has to be an instance ofMimeRender
forJSON
andUser
- In order for there to be an instance of
MimeRender
forJSON
andUser
there has to be an instance of Aeson'sToJSON
forUser
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
= _usersIndex :<|> _usersShow usersServer
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
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
[ = "Isaac Newton"
{ name = 372
, age = "isaac@newton.co.uk"
, email = "isaac"
, username = fromGregorian 1683 3 1
, registrationDate
}User
, = "Albert Einstein"
{ name = 136
, age = "ae@mc2.org"
, email = "albert"
, username = fromGregorian 1905 12 1
, registrationDate
} ]
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]
= pure users usersIndex
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 User
s username
.
matchesUsername :: String -> User -> Bool
= (uname ==) . username matchesUsername uname
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
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
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 Symbol
s. 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.