The Haskell Servant library is a fantastic way to write web APIs.
When you’re implementing the handlers, you either need to write them in the Handler
monad, or define a Nat
ural transformation (a way of converting) your choice of monad into the Handler
monad.
The Handler
monad is a newtype
around ExceptT ServantErr IO a
, where ServantErr
is a way of providing errors like 404 -- Not Found
or 401 -- Not Authorized
, or other non-200 responses, like 302
redirection.
If you’re familiar with ExceptT
, this isn’t new to you.
You can always use throwError
in ExceptT
to short-circuit the block and return the given error.
Servant handles the ServantErr
intelligently, converting it into an appropriate response.
For non-ServantErr
exceptions, Servant lets the serving backend (typically WAI
) handle it, usually by providing a 500
error.
ExceptT e IO
antipatternPerhaps you’ve read Michael Snoyman’s Exceptions Best Practices In Haskell blog post.
Perhaps you’re sold on the idea – why bother with ExceptT
over IO
when IO
already can throw runtime errors?
Furthermore, maybe you’re concerned with performance – the >>=
implementation for ExceptT
must do case analysis on the result to determine what to do next.
Carter Schonwald’s monad-ste package provides a more efficient way of dealing with exceptions, as it uses GHC’s runtime exception system.
There are various good reasons why you might want to strip ExceptT
from your Servant handlers.
There are various good reasons why you wouldn’t want to do that.
I’m in the first camp – I don’t want ExceptT
over IO
.
Maybe you don’t even like monad transformers at all, and just want your handlers to be in plain ol’ IO
.
Well, it turns out, that doesn’t take much code!
The servant-server
library allows you to use the function enter
to provide a conversion function from one monad to another.
Given an API type like:
type API =
"best-numbers" :> Get '[JSON] [Int]
we can write a handler like this:
server :: IO [Int]
server = do
now <- getCurrentTime
let timeInSeconds = utctDayTime now
wakeUpTime = 8 * 60 * 60
when (timeInSeconds <= wakeUpTime) $
throwIO err400 {
errBody = "request too early!"
}
return [1,2,3]
This handler returns [1, 2, 3]
if it’s awake.
But if the current time is less than 8 AM UTCTime, then we throw a 400 error instead.
Since ServantErr
is an instance of Exception
, we’re allowed to throw it in IO
using throwIO :: Exception e => e -> IO a
.
If you’re using the exceptions
package, you can use throwM
as well.
To hook this up with the serve
function, we need to use enter
and provide a NT
natural transformation/conversion function.
The type signature in the documentation is super generic, but ultimately, we’re looking for a function like:
type NaturalTransformation source target
= forall a. source a -> target a
Or, in English, “a function that converts a source a
into a target a
that is forbidden from inspecting the a
values.”
Concretely, for our specific use case, we want:
convert :: IO a -> Handler a
Hole driven development to the rescue! HDD is where you create a type hole and fill it in with your ‘best guess’ based on the surrounding context. Typically you’ll drop another type hole, which allows you to interactively develop with the compiler.
convert :: IO a -> Handler a
convert action = _f
Well, _f
gives us a type hole for Handler a
, which isn’t surprising.
How can we construct a Handler
?
The Haddocks point us to an exposed constructor, also Handler
, which we can use.
convert :: IO a -> Handler a
convert action = Handler _f
Now, _f
is ExceptT ServantErr IO a
.
How do we construct an ExceptT
?
The Haddocks show that we have a constructor, also called ExceptT
, which expects an m (Either e a)
.
So let’s plug that in:
convert :: IO a -> Handler a
convert action = Handler (ExceptT _f)
Now _f
is IO (Either ServantErr a)
.
This is where it gets tricky.
We know that we have an IO a
on hand.
If we do a hoogle search for IO a -> IO (Either e a)
, then we get a bunch of funny results.
None of them are exactly right, but there are a lot of variants on try
.
So let’s hoogle for try
!
That gives us this nice definition:
try :: Exception e => IO a -> IO (Either e a)
so let’s plug that in:
convert :: IO a -> Handler a
convert action = Handler (ExceptT (try _f))
Now, _f
has the type IO a
.
And we have an IO a
already – it’s the parameter we’ve been passed!
So we can simplify our convert:
convert :: IO a -> Handler a
convert = Handler . ExceptT . try
wrap it in the NT
natural transformation newtype:
convert :: IO :~> Handler
convert = NT . Handler . ExceptT . try
and use it in enter
:
app :: Application
app = server (Proxy :: Proxy Api) (enter convert handler)
Voila! You’re throwing and catching exceptions in IO, and Servant is still getting the chance to handle them however it wants.