Natural transformations in Servant

- January 18, 2017
Kwang's Haskell Blog - Natural transformations in Servant

Natural transformations in Servant

Posted on January 18, 2017 by Kwang Yul Seo

I’ve recently started using servant at work. Servant lets us declare web APIs at the type-level once and use those APIs to write servers, obtains client functions and generate documentation. It’s a real world example which shows the power of Haskell type system.

The most interesting part of Servant is its extensible type-level DSL for describing web APIs. However, I found another interesting application of theory into practice in servant-server library. It is the use of natural transformation to convert one handler type into another handler type.

In Servant, Handler is a type alias for ExceptT ServantErr IO.

type Handler = ExceptT ServantErr IO

Thus Handler monad allows us to do:

  • Perform IO operations such as database query through the base monad IO.
  • Throw a ServantErr if something went wrong.

Here’s an example of a Servant handler.

type ItemApi =
    "item" :> Capture "itemId" Integer :> Get '[JSON] Item

queryItemFirst :: Integer -> IO (Maybe Item)
queryItemFirst itemId = ...

getItemById :: Integer -> Handler Item
getItemById itemId = do
  mItem <- liftIO $ queryItemFirst itemId
  case mItem of
    Just item -> return item
    Nothing   -> throwError err404

So far so good, but what if queryItemFirst needs a database connection to retrieve the item? Ideally, we would like to create a custom monad for our application such as

data AppEnv = AppEnv { db :: ConnectionPool }
type MyHandler = ReaderT AppEnv (ExceptT ServantErr IO)

queryItemFirst :: ConnectionPool -> Integer -> IO (Maybe Item)
queryItemFirst cp itemId = ...

getItemById :: Integer -> MyHandler Item
getItemById itemId = do
  cp <- db <$> ask
  mItem <- liftIO $ queryItemFirst cp itemId
  case mItem of
    Just item -> return item
    Nothing   -> throwError err404

Unfortunately, this does not work because serve wants Handler type. We need a way to transform MyHandler into Handler so that Servant can happily serve our handlers. Because both MyHandler and Handler are monads, we need a monad morphism. Or more generally, we need a natural transformation from MyHandler to Handler.

Servant provides a newtype wrapper Nat which represents a natural transformation from m a to n a.

newtype m :~> n = Nat { unNat :: forall a. m a -> n a}

So what we want is MyHandler :~> Handler.

myHandlerToHandler :: AppEnv -> MyHandler :~> Handler
myHandlerToHandler env = Nat myHandlerToHandler'
  myHandlerToHandler' :: MyHandler a -> Handler a
  myHandlerToHandler' h = runReaderT h env

Okay, now we can get a natural transformation MyHandler :~> Handler by applying an AppEnv to myHandlerToHandler. How can I tell the Servant to use this natural transformation to serve our handlers? That’s what enter does!

server :: AppEnv -> Server ItemApi
server env =
  enter (myHandlerToHandler env) getItemById

Wrapping Handler with ReaderT is a common idiom, so Servant provides a convenient function runReaderTNat which is exactly the same to myHandlerToHandler. So we can rewrite server as follows:

server :: AppEnv -> Server ItemApi
server env =
  enter (runReaderTNat env) getItemById

Servant also provides a lot of monad morphisms such as hoistNat, embedNat, squashNat and generalizeNat. Sounds familiar? These are just wrappers around mmorph library functions. Interested readers are referred to Gabriel Gonzalez’s article mmorph-1.0.0: Monad morphisms.

In object-oriented programming, we use Adapter pattern to allow the interface of an existing class to be used as another interface. In functional programming, we use natural transformations (or more generally, functors) to do so!

Read more