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 Int
s 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 theEither String Int
s intoExceptT String
s. -
withExceptT
to convert fromExceptT String
toExceptT ServantErr
as required byHandler
.
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