6

I'm a Haskell beginner and am currently using wreq to make a simple wrapper around an api. I want to send an if-modified-since header if provided with a time. I am doing so in the following manner.

getResponse :: (FormatTime t, Exception e) => File -> Maybe t -> IO (Either e (Response L.ByteString))
getResponse file (Just t) =
  let formattedTime = (B.pack . formatTime defaultTimeLocale rfc822DateFormat) t
      opts = defaults & header "if-modified-since" .~ [formattedTime]
  in try $ getWith opts $ buildUrl file

getResponse file Nothing = try $ (get $ buildUrl file)

I noticed that 304 (not modified) responses are coming back as exceptions so that was my justification for using the Either type. I wanted to provide visibility to errors for people that might use this api wrapper.

Assuming the request is successful, I want to parse the response body into a corresponding type defined in my library. There is a chance deserialization might not work correctly if something changes on the server I'm making requests to so I chose to use the Maybe type to account for this.

getPayload :: FromJSON b => (Either e (Response L.ByteString)) -> Either e (Maybe b)
getPayload (Left _)  = return Nothing
getPayload (Right a) = return $ fmap Just (^. responseBody) =<< asJSON a

The signatures of these functions are starting to seem like an eyesore to me, something in my gut is telling me there is a better way but I am unsure. One thing I did was make another function to put them together with the hopes it would be easier to use. This is the function I plan on using to create other functions to make more specific requests to individual resources.

getResource :: (Exception e, FormatTime t, FromJSON b) => File -> Maybe t -> IO (Either e (Maybe b))
getResource f t =  getPayload <$> (getResponse f t)

I now have to deal with 3 layers of structure when dealing with an http request. IO, Either, and Maybe. Am I making this too complicated? What can I do to make this less of a pain to work with from a usage and maintainability perspective? How can I improve this?

Nick Acosta
  • 1,890
  • 17
  • 19
  • 4
    Couldn't you flatten `Either e (Maybe b)` into `Either e b`, where the `e` can also carry information about deserialization failures? `Maybe b` is isomorphic to `Either () b`. – Mark Seemann Aug 25 '17 at 05:37
  • 2
    I'd consider using a custom data type `data ResourceResult b = Error e | NotModified | Result b`. Library sum and product types are fine, but are flavorless: they are so generic that they convey no meaning. It's better to use a custom type with proper, evocative names. – chi Aug 25 '17 at 11:52
  • Thank you for this recommendation @chi. I will keep this in mind. – Nick Acosta Aug 25 '17 at 21:04

1 Answers1

2

This may not be quite what you want, but asJSON has the return type m (Response a), where m is a MonadThrow. While Maybe is a MonadThrow instance, then so is Either e. This means that you don't have to use Maybe in order to handle if anything goes wrong with asJSON. You can 'stay' in the Either monad instead:

getPayload :: FromJSON b => Either SomeException (Response L.ByteString)
                         -> Either SomeException b
getPayload = ((fmap (^. responseBody) . asJSON) =<<)

Clearly, this puts an additional constraint on the type of error on the left side, so I'm not sure that this is acceptable. If not, please leave a comment.

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • I will try this tonight and get back to you. – Nick Acosta Aug 25 '17 at 21:00
  • After doing some reading on `MonadThrow` at https://www.schoolofhaskell.com/user/commercial/content/exceptions-best-practices It looks like MonadThrow will hide the exception that gets thrown. I don't want this to happen because if the exception is a 304 response code the consumer will need to know so they can serve what data they have cached. The idea of using a custom type sounds appealing. – Nick Acosta Aug 26 '17 at 00:49
  • 1
    @NickAcosta Yes, a custom type may be what you want, but it's hardly correct to say that `MonadThrow` will hide the exception. This is true for `Maybe`, because `Nothing` throws away the error cause, but for `Either e`, the exception is right there in the `Left` case, and can be handled with `catch`. – Mark Seemann Aug 26 '17 at 05:12
  • True. Im going to tinker some more later. Thanks for the time, effort and explanations. Have a good weekend. – Nick Acosta Aug 26 '17 at 05:45
  • 1
    @NickAcosta FWIW, using `MonadThrow` with exceptions *enables* a client to handle exceptions using `catch`, but it doesn't *force* it to do so. OTOH, if you use `Either` or a custom sum type like the one suggested by @chi, then you *force* the client to handle all outcomes of the operation. I tend to lean towards the latter, but it does depend on which goal you're trying to reach. – Mark Seemann Aug 26 '17 at 06:52