Suppose we have a webserver that can perform different kinds of operations on different kinds of values. Perhaps it can reverse or capitalize strings, and increment or negate integers:

# curl http://localhost:8081/example/reverse elpmaxe # curl http://localhost:8081/example/caps EXAMPLE # curl http://localhost:8081/1234/inc 1235 # curl http://localhost:8081/1234/neg -1234

Moreover, it can echo back any value:

# curl http://localhost:8081/example/echo example # curl http://localhost:8081/1234/echo 1234

However, it does not make sense to try to reverse an integer or increment a string; requesting either of

http://localhost:8081/1234/reverse http://localhost:8081/example/inc

should result in HTTP error.

This is an example of a dependently typed server: the value that we are given as input determines the type of the rest of the server. If we get a string as input, we expect a string operation as the second argument and the response is also a string; if we get an integer as input, we expect an integer operation as the second argument and the response is also an integer.

We can encode the type of values that are either strings or integers using the GADT

data Value :: * -> * where VStr :: Text -> Value Text VInt :: Int -> Value Int

and the type of operations on a particular kind of value as

data Op :: * -> * where OpEcho :: Op a OpReverse :: Op Text OpCaps :: Op Text OpInc :: Op Int OpNeg :: Op Int

The core of our server will be:

execOp :: Value a -> Op a -> Value a OpEcho = val execOp valval VStr str) OpReverse = VStr $ Text.reverse str execOp (str)Text.reverse str VStr str) OpCaps = VStr $ Text.toUpper str execOp (str)Text.toUpper str VInt i) OpInc = VInt $ i + 1 execOp (i) VInt i) OpNeg = VInt $ negate i execOp (i)

Note that the type of execOp reflects precisely that the type of the input matches both the type of the operation we’re applying and the type of the result.

In the remainder of this blog post we will explain which combinators we need to add to Servant to be able to define such a dependently typed webserver. We will assume familiarity with the implementation of servant; see Implementing a minimal version of haskell-servant for an introduction. We will also assume familiarity with the following language extensions:

{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-}

all of which we will need in this development.

Preliminaries

We will need some reasonably standard preliminary definitions.

Existentials

Consider parsing a Value ; what would be the type of

fromText :: Text -> Value ???

Clearly, the type argument to Value here depends on the actual run-time value of the Text . Since we do not know this type argument before parsing, we need to hide it in an existential. We can define a general existential wrapper as

data Some :: ( * -> * ) -> * where Some :: f a -> Some f f a

We can then define a FromText instance as follows:

instance FromText ( Some Value ) where = Just $ case readMaybe (Text.unpack t) of fromText treadMaybe (Text.unpack t) Just n -> Some $ VInt n Nothing -> Some $ VStr t

Reified dictionaries

Consider trying to convince the type checker that all values (i.e., String s and Int s in our running example) admit an equality test. One way to do this is to use reified type class dictionaries:

data Dict ( c :: Constraint ) where Dict :: c => Dict c

Using Dict we can define

valueEq :: Value a -> Dict ( Eq a) a) VStr _) = Dict valueEq (_) VInt _) = Dict valueEq (_)

The first use of Dict in valueEq captures the in-scope Eq String constraint, and the second use captures the in-scope Eq Int constraint. We can bring these constraints back into scope by pattern matching:

useEq :: Dict ( Eq a) -> a -> a -> Bool a) Dict = ( == ) useEq

Simulating type-level lambda

In order to define the type dependency in the webserver we will need a type level function f that says “if the input has type a, then the remainder of the server has type f a”. We might try to use a type synonym for this:

type ExecOp a = Capture "op" ( Op a) a) :> Get '[ PlainText ] ( Value a) '[] (a)

Unfortunately, we will need to be able to refer to ExecOp without an argument, which is not possible in Haskell: type synonyms must always be fully applied. We could use a newtype , but that will lead to other headaches ( HasServer cannot be derived).

Instead, we will apply defunctionalization and use a datatype to symbolically denote a particular type-level function, and then use a type family to implement application of the symbolic function to an argument:

data ExecOp type family Apply f a :: * type instance Apply ExecOp a = Capture "op" ( Op a) a) :> Get '[ PlainText ] ( Value a) '[] (a)

As an example of using Apply , given the definition of execOp above, we can define the following wrapper:

serveExecOp :: Value a -> Server ( Apply ExecOp a) a) = return $ execOp val op serveExecOp val opexecOp val op

This use of defunctionalization to simulate type-level lambdas was pioneered in the work on the singletons package (see Promoting Functions to Type Families in Haskell).

Dependently typed servers

A dependently typed server is a server with some argument, such that the value of that argument determines the type of the server. We can encode this as

newtype DepServer ( ix :: * -> * ) ( f :: * ) ( m :: * -> * ) = ) () ( DepServer ( forall a . ix a -> ServerT ( Apply f a) m) ix af a) m)

Here ix is the type of the index ( Value in our running example), and f is the (dependent) type of the server (in our example, this will be ExecOp ). The m parameter is Servant’s standard server monad argument.

HasDepServer

We now introduce a new type class, alongside the standard Servant standard HasServer , that corresponds to dependent servers. The key idea is that we will need a HasServer instance for all valid instantiations of the type argument:

class HasDepServer ix f where ix f hasDepServer :: Proxy f -> ix a -> Dict ( HasServer ( Apply f a)) ix af a))

Let’s consider an example:

instance HasDepServer Value ExecOp where VStr _) = Dict hasDepServer _ (_) VInt _) = Dict hasDepServer _ (_)

In order to show we can construct servers for ExecOp , we need to show that we have a HasServer instance for all valid indices; in our case, that is String and Int . In other words, we need to show we have HasServer instances for both of

Capture "op" ( Op String ) :> Get '[ PlainText ] ( Value String ) '[] ( Capture "op" ( Op Int ) :> Get '[ PlainText ] ( Value Int ) '[] (

both of which we get (almost) for free from Servant. Hence, we can simply pattern match on the index and trivially construct the dictionary. This follows precisely the same strategy as in the valueEq example.

Dependent captures

We’re almost there now. We can now introduce a dependent version of capturing part of a URL:

data DepCapture ( ix :: * -> * ) ( f :: * ) ) (

(For simplicity’s sake this combines the functionality of (:>) and Capture in a single combinator. It would be possible to separate this out.)

With this combinator we can define the API we want:

type API = DepCapture Value ExecOp

The (somewhat simplified) HasServer instance for the standard, non-dependent, Capture looks like this:

instance ( FromText a, HasServer f) => HasServer ( Capture a :> f) a,f)f) type ServerT ( Capture a :> f) m = a -> ServerT f m f) mf m = ... route

The corresponding HasServer for DepCapture follows this closely. First, we note that the server for a dependent capture must be a dependent server:

instance ( FromText ( Some ix), HasDepServer ix f) ix),ix f) => HasServer ( DepCapture ix f) where ix f) type ServerT ( DepCapture ix f) m = DepServer ix f m ix f) mix f m

(We need the DepServer newtype wrapper because we are not allowed to have a polymorphic type as the right hand side of a type family.)

In the router we attempt to parse the index; if this succeeds, we unwrap the existential, discovering the type index we need for the rest of the server, use the HasDepServer type class to get a HasServer instance for this particular type index, and continue as usual:

Proxy ( DepServer subserver) request respond = routesubserver) request respond case processedPathInfo request of processedPathInfo request : ps) -> (pps) case fromText p :: Maybe ( Some ix) of fromTextix) Nothing -> $ failWith NotFound respondfailWith Just ( Some ( p' :: ix a)) -> ix a)) case hasDepServer ( Proxy :: Proxy f) p' of hasDepServer (f) p' Dict -> route ( Proxy :: Proxy ( Apply f a)) route (f a)) (subserver p') = ps } request{ pathInfops } respond _ -> $ failWith NotFound respondfailWith

Wrapping up

To define the full server, we just need to wrap serveExecOp in the DepServer constructor and we’re done:

server :: Server API = DepServer serveExecOp serverserveExecOp