Either computations in servant handler

A servant-server Handler is a newtype wrapper over an ExceptT, and has instances for MonadThrow, MonadCatch, MonadError, etc.

This might be a somewhat contrived example, but it shows an issue I often face:

In a handler I want to call three functions that return Either String Int, then perform a computation of type Int -> Int -> Int -> IO (Either SomeError Text), taking the three Ints from before.

How should I structure this code to ensure that an error is returned as early as possible?

I see that I can use Either's Monad instance to “collapse” the first three Either String Int computations into e.g. Either String (Int,Int,Int), and then binding the IO computation to some result value and then use case to decide whether to return a successful result or use throwError to throw the SomeError type (after a conversion?), but I was hoping to be able to something like the following:

f, g, h :: Either String Int
a :: Int -> Int -> Int -> IO (Either SomeError Text) 

myHandler :: Handler Text
myHandler = do
    x1 <- f
    x2 <- g
    x3 <- h
    liftIO $ convertError $ (a x1 x2 x3)

Is it possible to write it similar to the code above?


Solution 1:

Assuming you have a function strToServantErr :: String -> ServantErr for converting the errors returned by f,g,h into errors that can be returned by your handler, then we can use:

  • liftEither to get the Either String Ints into ExceptT Strings.
  • withExceptT to convert from ExceptT String to ExceptT ServantErr as required by Handler.
x1 <- withExceptT strToServantErr $ liftEither f

As you're doing this three times, we can make it neater using mapM:

[x1, x2, x3] <- mapM (withExceptT strToServantErr . liftEither) [f, g, h]

Now that we've sorted the arguments, we can use the same idea to fix the return. Renaming your convertError function to someErrorToServantErr for uniformity and assuming it has type SomeError -> ServantErr, then we can do:

result <- liftIO $ a x1 x2 x3
withExceptT someErrorToServantErr $ liftEither result

We unwrap the IO computation of a, then lift it to ExceptT and convert the exception type.

After tidying away some of the code into a helper function, this gives us something like:

myHandler :: Handler Text
myHandler = do
    [x1, x2, x3] <- mapM (liftMapE strToServantErr) [f, g, h]
    eitherResult <- liftIO $ a x1 x2 x3
    liftMapE someErrorToServantErr eitherResult
  where liftMapE f = withExceptT f . liftEither

Which will fail ASAP with a converted error as desired, and while it's dense is hopefully not all that unreadable.


You could also go the Applicative route, although I can't find a way of making it particularly nice (I've not used applicative functors much though, I'm probably missing some useful tricks):

myHandler :: Handler Text
myHandler = do
    let [x1, x2, x3] = map (liftMapE strToServantErr) [f, g, h] -- [Handler Int]
    tmp <- a <$> x1 <*> x2 <*> x3 -- IO (Either SomeError Text)
    eitherResult <- liftIO $ tmp
    liftMapE someErrorToServantErr eitherResult
  where liftMapE f = withExceptT f . liftEither

Any improvements on the above code are welcome!

Solution 2:

I believe a Handler construction is missing in hnefatl's answer (assuming the question was asked about Servant 0.15 and around). Notice

newtype Handler a = Handler { runHandler' :: ExceptT ServantErr IO a }

this is what i had to do

eitherToHandler :: (e -> ServantErr) -> Either e a -> Handler a
eitherToHandler f = Handler . withExceptT f . liftEither