-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
adds recipe CapabilityPatternWithCheckedExceptions #262
base: master
Are you sure you want to change the base?
Conversation
Thanks for your work @afcondon. At the same time, I don't think this recipe in its current state is what should be added here. Sorry, but I'm having difficulty figuring out what about this recipe just seems off to me. Perhaps it's because none of the exceptions raised are "real" exceptions? Perhaps its because one could use |
Fair comments - i, personally, think this pattern has a lot of potential but i also feel like i won't know if it turns out to be less alluring when used in real world application structuring. I mostly wanted to do it to check that it could be done - which i've done to my satisfaction with it where it's at now. Perhaps - after Miles' maybe has also weighed in? - we could open it up for discussion on Discourse instead and see what some further critical evaluation of it leads to. Either way, thanks for looking at it. |
Diving into this is on my backlog. Hopefully will be able to offer more meaningful review feedback in a week or so. Edit: It might be most efficient to review this after after your presentation on the topic (if that's still in the works) in next month's meetup. |
That's a good idea. I may well have some hands on experience of trying to use it in an application by then, too. |
@afcondon Any updates on this PR? I know you gave your presentation a while back, so did you get any feedback that might influence how we should proceed here? |
I didn't get any feedback positive or negative on the checked exceptions aspect. I'm sort of inclined to believe that "app skeletons" like this belong in a slightly different collection than the cookbook. But that said, i think that my example here would at the very least save someone a lot of work figuring out how to scaffold such a design, thus lowering the barrier to starting a project, which - at least to my mind - is really core value of cookbook recipes. So i'd be happy to take feedback to get this into the cookbook for now. with regard to your original objections, @JordanMartinez, perhaps the we could get someone to write a counterpoint recipe that does as you suggest with interleaving using |
I decided to checkout your branch locally to more easily navigate through it. Here's what I think. First, I think this recipe should be merged into the cookbook because the idea is worth it. However, it shouldn't be merged in its present form. Second, one problem with the recipe that can be fixed is how you currently handle any errors. Right now, you merge an error value with a valid output value rather than keeping the two separate. I think the code should distinguish these two. Let me walk through this in a bit more detail. program :: forall m.
Logger m =>
GetUserName m =>
m String
program = do
log "what is your name?"
name <- getUserName -- <-
log $ "Your name is " <> getName name
pure $ getName name In the instance for your monad, you handle all possible errors by doing some effects and then "returning" a default instance getUserNameAppExcVM :: GetUserName (AppExcVM var) where
getUserName = do
env <- ask
-- I want to focus on the below two lines
name <- safe ((getPureScript env.url) `handleError`
(errorHandlersWithDefault "there was an error!"))
pure $ Name name
-- Only showing the type signature
getPureScript :: forall m r
. Monad m
=> MonadHttp m
=> MonadFs m
=> String -> ExceptV (HttpError + FsError + r) m String
-- Only showing part of the type signature
errorHandlersWithDefault :: forall m a.
MonadEffect m =>
a ->
{ fsFileNotFound :: String -> m a
, fsPermissionDenied :: Unit -> m a
, httpNotFound :: Unit -> m a
, httpOther :: { body :: String, status :: Int} -> m a
, httpServerError :: String -> m a
}
errorHandlersWithDefault defaultValue = {
httpServerError: \error -> do
Console.log $ "Server error:" <> error
pure defaultValue
, -- and similarly for the rest of them
} Thus, Third and most importantly, you've hard-coded the monad to get ∷ ∀ r m.
MonadHttp m ⇒
String ->
ExceptV (HttpError + r) m String
get url = -- code In normal situations, we wouldn't hard-code the monad. Rather, we would use type class constraints via get ∷ ∀ r m.
MonadHttp m ⇒
MonadError (Variant (HttpError + r)) m ⇒
String ->
m String
get url = -- code But I think this is where things get nuanced when it comes to type classes. But before I say anything more, I should probably state my understanding on the two kinds of errors we could throw. First, there are some errors you could throw and which shouldn't be "caught" (i.e. unrecoverable errors). As an example, if your program requires the Second, there are some errors you could throw in the With that in mind, here's the nuance I'm not sure about. I think we would want something like this, but I don't think it would compile because get ∷ ∀ otherUnrecoverableErrors otherRecoverableErrors m.
MonadHttp m ⇒
-- MonadThrow = unrecoverable errors handled in `Main` layer
MonadThrow (Variant (ServerError + otherUnrecoverableErrors)) m ⇒
-- MonadError = recoverable errors handled in `API` layer
MonadError (Variant (TimeoutError + otherRecoverableErrors)) m ⇒
String ->
m String
get url =
-- pseudo code. Try getting the URL 3 times, retrying when it fails with
-- a recoverable TimeoutError. Otherwise, fail with ServerError,
-- which will be handled in `Main` layer.
tryNTimesThenFailWith 3
(Node.HTTP.get url)
(throwError (V.inj _serverError $ "Could not connect to " <> url)) I think the only solution to this problem is to remove Assuming I'm correct about the third point's type class nuance above, what can we do to get this PR merged? Two things.
In other words, for 2, this means redefining runApp :: forall a. AppExcVM () a -> Environment -> Aff a
runApp = runAppExcVM (RProxy :: _ ())
where
runAppExcVM :: forall var rl.
RowToList var rl =>
VariantTags rl =>
VariantShows rl =>
RProxy var ->
AppExcVM var a ->
Environment ->
Aff a
runAppExcVM _ (AppExcVM appExcVM) env =
runExceptT $ runReaderT appExcVM env
main :: Effect Unit
main = launchAff_ do
result <- AppExcVM.runApp program { url: "http://www.purescript.org" }
case result of
Right _ -> pure unit
Left e -> handleUnrecoverableError e
where
handleUnrecoverableError = V.match
{ httpServerError: \error -> do
Console.log $ "Server error:" <> error
, httpNotFound: \error -> do
Console.log "Not found"
, httpOther: \error -> do
Console.log $ "Other: { status: " <> show error.status <> " , body: " <> error.body <> "}"
, fsFileNotFound: \error -> do
Console.log $ "File Not Found" <> error
, fsPermissionDenied: \error -> do
Console.log "Permission Denied"
} Thoughts? |
That's a wonderfully detailed comment and analysis, @JordanMartinez, i've been meaning to reply for the last couple of days but haven't had time. I'll try and write a response and/or incorporate your ideas above over the weekend or next week. (btw this might be a good thing to cross-post to Discourse, cause probably there are more people interested in the ideas than those of us subbed to this GitHub issue) |
@afcondon No worries! Whenever you get to it. We can crosspost once we've discussed this further and have a final summary for things. |
I'm also wondering whether error handling has any similarities with contravariant logging. Rather than logging, we might have contravariant error handling. |
submission for #261