Either computations in servant handler
Asked Answered
T

2

6

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?

Trainor answered 28/3, 2018 at 6:43 Comment(1)
You might have an issue in that Handler is an ExceptT ServantErr, but your errors are Strings. Can we assume you have a function String -> ServantErr that you can use to convert the errors?Hedden
H
2

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!

Hedden answered 28/3, 2018 at 9:46 Comment(2)
You can go even further and use your custom error type in an ExceptT monad (or something similar) and use hoistServer (see here).Inducement
@AlpMestanogullari Ooh, cool. I don't actually know anything about servant, so hopefully OP will see your comment and find it useful.Hedden
G
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
Greenstein answered 19/1, 2022 at 18:56 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.