Skip to content

Conversation

@mdimjasevic
Copy link
Contributor

@mdimjasevic mdimjasevic commented Aug 9, 2018

This implements error handling with an Either in function return types to reflect the possibility of failure.

ksaric and others added 30 commits July 23, 2018 16:04
Copy link
Contributor

@ksaric ksaric left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some fixes required.

, transformers-base
, unliftio
--
--
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, it's best to leave unchanged lines so they don't confuse people.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not something I've done by hand. Rather, it was done automatically by a tool while I work on the project, I guess by the editor.

import Data.Aeson (parseJSON)
import Data.Aeson.Text (encodeToLazyText)
import Data.List (nub)
import Data.Text (pack)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/HttpLayer.hs Outdated
:: Config
-> Text
-> Either String Request
apiRequest Config{..} u = mapLeft show $ catches buildRequest handlerList
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need an Either here. All our URLs are "internal" and will fail if incorrect anyway.
I would otherwise consider this, but it seems to be difficult for you right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are multiple external functions that get called there and they can fail with an HttpException or with a JSONException. We shouldn't be hoping it will always be a happy case.

Copy link
Contributor

@ksaric ksaric Aug 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where in apiRequest does it say that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a handlerList in the body of the function providing these two types of exceptions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. And how can those exceptions reach this handlerList?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm getting something wrong, but there is this function call: catches buildRequest handlerList. In case buildRequest results in an exception given in the handlerList, it gets handled by a corresponding handler.

apiRequestAbsolute
:: Config
-> Text
-> Either String Request
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean with "Same here"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, as in "think why you return the Either". Why do you return it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the implementation of this function. Now it is almost the same as the apiRequest function, hence it is clear why Either in the return type.

-- | Request PUT
addJsonBody :: forall a. ToJSON a => a -> Request -> Request
addJsonBody body req = setRequestBodyJSON body $ setRequestMethod "PUT" req
addJsonBody :: forall a. ToJSON a => a -> Request -> Either String Request
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general rule, if you just wrap Either with a single constructor (Right), you probably don't need it.
Can you rewrite this to catch any exceptions and in that case to return it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your observation. However, an exception can occur and that's why there is a call to the catch function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And where do you catch it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's caught right there, on the first line of addJsonBody.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And where in the type signature does it say that this can fail?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type signature contains the return type Either String Request. The String part of the Either sum type represents its left side, which by convention represents a failure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mdimjasevic Think about what I'm asking here, don't just answer back - what place is that exception coming from? How do you know that this function needs to address this exception? What kind of signature is on this function, and does that tell you something about its behavior?
Think about exceptions - do they occur on pure functions? In which cases? A lot of things to think about, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these questions are easy to answer by peeking at the code. Functions from the HTTP library that are called in addJsonBody can throw exceptions, hence addJsonBody has a corresponding signature. But you must be right, these are great questions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functions from the HTTP library

Which ones?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addJsonBody function's signature follows from your requirement that the HTTPNetworkLayer.hnlAddJsonBody field should have a type hnlAddJsonBody :: forall a. (ToJSON a) => a -> Request -> Either String Request: https://iohk.myjetbrains.com/youtrack/issue/TSD-85#comment=93-26226


let url = showURL $ TicketCommentsURL tId
let req = apiRequest cfg url
case req of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chain computation?

let req = apiRequest cfg url

apiCall parseJSON req
case req of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chain computation?

let req = addJsonBody responseTicket (apiRequest cfg url)
void $ apiCall (pure . encodeToLazyText) req
let req = apiRequest cfg url
case req of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chain computation?

case nextPage of
Just nextUrl -> go page0 nextUrl
Nothing -> pure page0
case req' of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chain computation?

apiCall <- asksHTTPNetworkLayer hnlApiCall

Just <$> apiCall parseTicket req
case req of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chain computation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please explain what you mean with "Chain computation?"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Means, "use a Monad". You first need to address the other issues.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These computations are chained. If they were not, the code wouldn't compile.

Here we have the resulting monad m and the Either monad. They could be combined in a monad stack, but given that there is a single point where a failure could happen, a monad stack would be overkill. Don't you agree?

Copy link
Contributor

@ksaric ksaric left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some small fixes.

src/HttpLayer.hs Outdated
:: Config
-> Text
-> Either String Request
apiRequest Config{..} u = mapLeft show $ catches buildRequest handlerList
Copy link
Contributor

@ksaric ksaric Aug 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where in apiRequest does it say that?

src/HttpLayer.hs Outdated

-- | The mapLeft function takes a function and applies it to an Either value
-- iff the value takes the form Left _.
mapLeft :: (a -> c) -> Either a b -> Either c b
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In most cases, you don't need such functions, if you need to collapse either you use the either function, otherwise you can case match. Introducing functions for common operations just brings confusion.

-- | Request PUT
addJsonBody :: forall a. ToJSON a => a -> Request -> Request
addJsonBody body req = setRequestBodyJSON body $ setRequestMethod "PUT" req
addJsonBody :: forall a. ToJSON a => a -> Request -> Either String Request
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And where do you catch it?

let req = apiRequest cfg url

apiCall parseJSON req
res <- runEitherT $ do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can simplify if you want to use transformers here and remove transformers-either:

    res <- runExceptT $ do
        req <- ExceptT . pure $ apiRequest cfg url
        ExceptT $ apiCall parseJSON req

But I don't think we will require this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

res <- runExceptT $ (ExceptT . pure $ apiRequest cfg url) >>= ExceptT . apiCall parseJSON

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting it all into a single line doesn't seem like a good idea because it is harder to read.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I know. The reason it's there is because it has nice symmetry, the one that's relevant and tells you how you can get rid of transformers-either and where it says that we won't require this is one above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll make the change you suggested above.

Copy link
Contributor

@ksaric ksaric left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Further explanations.

src/HttpLayer.hs Outdated
:: Config
-> Text
-> Either String Request
apiRequest Config{..} u = mapLeft show $ catches buildRequest handlerList
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. And how can those exceptions reach this handlerList?

src/HttpLayer.hs Outdated
case parseEither parser v of
Right o -> pure o
Right o -> pure $ Right o
Left e -> error $ "couldn't parse response "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the result of parseEither parser v?

-- | Request PUT
addJsonBody :: forall a. ToJSON a => a -> Request -> Request
addJsonBody body req = setRequestBodyJSON body $ setRequestMethod "PUT" req
addJsonBody :: forall a. ToJSON a => a -> Request -> Either String Request
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And where in the type signature does it say that this can fail?

Copy link
Contributor

@ksaric ksaric left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes.

apiCall <- asksHTTPNetworkLayer hnlApiCall

Just <$> apiCall parseTicket req
case req of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Means, "use a Monad". You first need to address the other issues.

-- | Request PUT
addJsonBody :: forall a. ToJSON a => a -> Request -> Request
addJsonBody body req = setRequestBodyJSON body $ setRequestMethod "PUT" req
addJsonBody :: forall a. ToJSON a => a -> Request -> Either String Request
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mdimjasevic Think about what I'm asking here, don't just answer back - what place is that exception coming from? How do you know that this function needs to address this exception? What kind of signature is on this function, and does that tell you something about its behavior?
Think about exceptions - do they occur on pure functions? In which cases? A lot of things to think about, no?

apiRequestAbsolute
:: Config
-> Text
-> Either String Request
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, as in "think why you return the Either". Why do you return it?

src/HttpLayer.hs Outdated
case parseEither parser v of
Right o -> pure o
Right o -> pure $ Right o
Left e -> error $ "couldn't parse response "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like that matches the return type of the function (hint, hint) 😏

src/HttpLayer.hs Outdated
:: Config
-> Text
-> Either String Request
apiRequest Config{..} u = showErr $ catches buildRequest handlerList
Copy link
Contributor

@ksaric ksaric Aug 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing as #50 (comment)

@ksaric
Copy link
Contributor

ksaric commented Aug 28, 2018

Closing in favor of #57

@ksaric ksaric closed this Aug 28, 2018
@ksaric ksaric deleted the mdimjasevic/TSD-85-take-2 branch August 28, 2018 13:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants