3

I'm currently trying to implement a simple web server with servant. At the moment, I have a IO (Maybe String) that I want to expose via a GET endpoint (this might be a database lookup that may or may not return a result, hence IO and Maybe). If the Maybe contains a value, the response should contain this value with a 200 OK response status. If the Maybe is Nothing, a 404 Not Found should be returned.

So far I was following the tutorial, which also describes handling errors using throwError. However, I haven't managed to get it to compile. What I have is the following code:

type MaybeAPI = "maybe" :> Get '[ JSON] String

server :: Server MaybeAPI
server = stringHandler

maybeAPI :: Proxy MaybeAPI
maybeAPI = Proxy

app :: Application
app = serve maybeAPI server

stringHandler :: Handler String
stringHandler = liftIO $ fmap (\s -> (fromMaybe (throwError err404) s)) ioMaybeString 

ioMaybeString :: IO (Maybe String)
ioMaybeString = return $ Just "foo"

runServer :: IO ()
runServer = run 8081 app

I know this is probably more verbose than it needs to be, but I guess it can be simplified as soon as it is working. The problem is the stringHandler, for which the compilation fails with:

No instance for (MonadError ServerError []) arising from a use of ‘throwError’

So my question is: Is this the way to implement such an endpoint in Servant? If so, how can I fix the implementation? My Haskell knowledge is rather limited and I never used throwError before, so it's entirely possible that I'm missing something here. Any help is appreciated!

l7r7
  • 1,134
  • 1
  • 7
  • 23
  • Your problem is actually rather more fundamental than anything that's specific to Servant or its error handling. [fromMaybe](https://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Maybe.html#v:fromMaybe) has type `a -> Maybe a -> a`, and here your `a` has to be a `String`. This means that `throwError err404`, as the first argument to `fromMaybe`, has to itself be if type `String`, which it clearly isn't . – Robin Zigmond Jan 18 '20 at 22:43
  • @RobinZigmond Is there another way to turn the `Maybe String` into `IO String` extracting the value if it's a `Just` and using `throwError err404` if it's `Nothing`? – l7r7 Jan 18 '20 at 22:47
  • Yes there is, and very simply - I'm just finishing up an answer at the moment. – Robin Zigmond Jan 18 '20 at 22:51

1 Answers1

3

As I mentioned in my comment, the problem is that in the offending line:

stringHandler :: Handler String
stringHandler = liftIO $ fmap (\s -> (fromMaybe (throwError err404) s)) ioMaybeString 

s is a Maybe String, so to use it as the second argument to fromMaybe, the first argument must be a String - and throwError is never going to produce a string.

Although you talked about your code perhaps being too verbose and you would look at simplifying it later, I think part of the problem here is that in this particular handler you are trying to be too concise. Let's try to write this in a more basic, pseudo-imperative style. Since Handler is a monad, we can write this in a do block which checks the value of s and takes the appropriate action:

stringHandler :: Handler String
stringHandler = do
    s <- liftIO ioMaybeString
    case s of
        Just str -> return str
        Nothing -> throwError err404

Note that throwError can produce a value of type Handler a for any type it needs to be, which in this case is String. And that we have to use liftIO on ioMaybeString to lift it into the Handler monad, else this won't typecheck.

I can understand why you might have thought fromMaybe was a good fit here, but fundamentally it isn't - the reason being that it's a "pure" function, that doesn't involve IO at all, whereas when you're talking about throwing server errors then you are absolutely unavoidably doing IO. These things essentially can't mix within a single function. (Which makes the fmap inappropriate too - that can certainly be used to "lift" a pure computation to work on IO actions, but here, as I've said, the computation you needed fundamentally isn't pure.)

And if you want to make the stringHandler function above more concise, while I don't think it's really an improvement, you could still use >>= explicitly instead of the do block, without making the code too unreadable:

stringHandler = liftIO ioMaybeString >>= f
    where f (Just str) = return str
          f Nothing = throwError err404
Robin Zigmond
  • 17,805
  • 2
  • 23
  • 34
  • Thank you for the explanation, I think I get what you're trying to say with the pure vs impure functions. However, your suggested code for `stringHandler` doesn't compile for me. For the line with `throwError` it says _Couldn't match type ‘GHC.IO.Exception.IOException’ with ‘ServerError’_. Can you help me with that? – l7r7 Jan 18 '20 at 23:06
  • Hmm, sorry about that, give me a little while to see if I can figure out what's wrong. – Robin Zigmond Jan 18 '20 at 23:10
  • Sure, no problem. I also tried your second suggestion with `>>=` instead of the `do` block, which gives me _parse error (possibly incorrect indentation or mismatched brackets)_. This is even more confusing than the other error message where I can roughly understand the problem – l7r7 Jan 18 '20 at 23:15
  • Oh, for that one I just noticed I have a typo with `==` where there should be an `=`. I'll edit to fix that, I can't see anything else wrong so hopefully it's just that. (Still thinking about the `throwError` one, can't yet see why it's complaining about it :/ ) – Robin Zigmond Jan 18 '20 at 23:17
  • Ah yes, without the typo and the `FlexibleContexts` language extension I get the same error message – l7r7 Jan 18 '20 at 23:21
  • ok, I think I've figured it out, apologies for the delay - I think going through the `IO` monad is not needed here and is causing the problem, it should work if we just use the `Handler` monad directly. Just to confirm: does the error message refer to a "functional dependency" somewhere? (It should do if what I'm thinking is correct.) – Robin Zigmond Jan 18 '20 at 23:46
  • Yes: Couldn't match type ‘GHC.IO.Exception.IOException’ with ‘ServerError’ arising from a functional dependency between: constraint ‘MonadError ServerError IO’ arising from a use of ‘f’ instance ‘MonadError GHC.IO.Exception.IOException IO’ at – l7r7 Jan 18 '20 at 23:57
  • Thanks for confirming. I've just rewritten the answer in a way that hopefully should now work. The reason it didn't before was essentially that the `do` block was working in the `IO` monad whereas `throwError` requires the `Handler` monad. So the solution is to not involve `IO` but use `Handler` directly - which in this case has required a slightly awkward use of `liftIO` on `ioMaybeError` in order to be able to use it. – Robin Zigmond Jan 19 '20 at 00:01