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
change the type of generalBracket, fixes #63 #64
Conversation
I want to reuse them
To make sure that the all the effects performed while releasing a resource are preserved, not just the IO effects. I'm writing the tests first, so the tests currently fail.
It is easy to accidentally implement the 'ExceptT' instance in a way which causes the error message thrown during 'use' to get propagated instead of the one thrown by 'release'. When both 'use' and 'release' throw an IO exception, the one thrown by 'release' wins, so I think we should follow suit.
Now that we have documentation for individual instances, there is no need to repeat this seldom-used information here.
The exceptions package used to have an undocumented advantage over lifted-base, namely that the non-IO effects are not discarded, but then exceptions-0.9.0 made a breaking change which lost this advantage. In this PR, I am restoring this advantage, and I am documenting it, both to help users decide which package to pick, and so we don't foolishly repeat that mistake.
This is the PR's main change. I make the type of 'generalBracket' even more general, which allows me to fix the implementations of 'StateT' and 'WriterT' so they don't discard their state changes.
if ExceptT has one, MaybeT should have one too
Update the documentation to reflect the difference between an exception and an error, calling out the cases in which the user might expect to catch all errors but will only catch all exceptions. In particular, 'onException' will only run the handler when an exception is thrown but not when any other kind of error is thrown; this is not particularly useful, but we can't do better without changing its type signature. A new function 'onError' is introduced which does not have this flaw.
to match the style of the existing codebase
src/Control/Monad/Catch.hs
Outdated
-- exception, or abort for some other reason. For example, in @ExceptT e IO@ | ||
-- you can use 'throwM' to abort with an exception or | ||
-- 'Control.Monad.Trans.Except.throwE' to abort with a value of type 'e'. | ||
data ExitCase a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about a derived Show
instance for this, for debugging purposes?
src/Control/Monad/Catch.hs
Outdated
-- | A 'MonadMask' computation may either succeed with a value, abort with an | ||
-- exception, or abort for some other reason. For example, in @ExceptT e IO@ | ||
-- you can use 'throwM' to abort with an exception or | ||
-- 'Control.Monad.Trans.Except.throwE' to abort with a value of type 'e'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explicitly state which constructors of ExitCase
correspond to throwE
and throwM
.
src/Control/Monad/Catch.hs
Outdated
@@ -194,61 +214,95 @@ class MonadCatch m => MonadMask m where | |||
-- tranformers from having @MonadMask@ instances (notably | |||
-- multi-exit-point transformers like 'ExceptT'). If you are a | |||
-- library author, you'll now need to provide an implementation for | |||
-- this method. As two examples, here is a @ReaderT@ implementation: | |||
-- this method. The @StateT@ implementation demonstrates most of the | |||
-- subtelties: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/subtelties/subtleties
src/Control/Monad/Catch.hs
Outdated
-- | ||
-- The only effect which is intentionally not incorporated in the @release@ | ||
-- action is the effect of throwing an error. In that case, the error must be | ||
-- re-thrown. One subtelty which is easy to miss is that in the case in which |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/subtelty/subtlety
src/Control/Monad/Catch.hs
Outdated
@@ -653,44 +783,56 @@ catches a hs = a `catch` handler | |||
|
|||
-- | Run an action only if an exception is thrown in the main action. The | |||
-- exception is not caught, simply rethrown. | |||
-- | |||
-- /NOTE/ The action is only run if an /exception/ is throw. If the monad |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/is throw/is thrown/
CHANGELOG.markdown
Outdated
@@ -1,3 +1,13 @@ | |||
????? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defintely version 0.10.0, as this will require a major version bump.
I've left some comments inline. I'd definitely like @snoyberg to look over this is as well.
Can you be more specific on what your intent is? |
onException :: MonadCatch m => m a -> m b -> m a | ||
onException action handler = action `catchAll` \e -> handler >> throwM e | ||
|
||
-- | Run an action only if an error is thrown in the main action. Unlike |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be worth spelling out that the difference is for transformers like EitherT
and MaybeT
that have multiple exit points.
src/Control/Monad/Catch.hs
Outdated
-> (a -> m b) | ||
-- ^ inner action to perform with the resource | ||
-> m b | ||
-> (a -> ExitCase b -> m c) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should have commented on this earlier. I think it would make more sense to move the release action to before the use action, to match more closely the type signature of bracket
in base
.
src/Control/Monad/Catch.hs
Outdated
-- functions are ignored. | ||
-- | A generalized version of 'bracket' which uses 'ExitCase' to distinguish | ||
-- the different exit cases, and returns the values of both the 'use' and | ||
-- 'release' actions. You should probably use 'bracket' instead. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd probably clarify why bracket
is better to use (more ergonomic), and comment that many other functions (like finally
) are built on top of this method.
src/Control/Monad/Catch.hs
Outdated
-> m b | ||
-> (a -> ExitCase b -> m c) | ||
-- ^ release the resource, observing the outcome of the inner action | ||
-> m (b, c) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this signature makes things much easier to understand.
With
But now that I'm thinking about this more, I don't think it makes a lot of sense to add a variant to the |
OK. I'm not exactly sure what form your suggestion is going to take on, but I'd prefer to keep this PR about only the changes needed to replace the type signature for |
To clarify: I no longer have a suggestion for changing |
the Either instance has one, so MaybeT and ExceptT should have one as well
I have now addressed all the inline comments. I have also added an entry to the changelog about the new |
src/Control/Monad/Catch.hs
Outdated
-- example, if @f@ is an 'ExceptT' computation which aborts with a 'Left', the | ||
-- computation @onError f g@ will execute @g@, while @onException f g@ will not. | ||
-- | ||
-- For monads which, unlike 'ExceptT' and 'MaybeT', only have a single exit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The phrasing of this sentence is a little awkward. I would prefer stating it like:
"This distinction is only meaningful for monads which have multiple exit points, such as 'Except' and 'MaybeT'. For monads that only have a single exit point, there is no difference between 'onException' and 'onError', except that 'onError' has a more constrained type."
-> (a -> m b) | ||
-- ^ inner action to perform with the resource | ||
-> m b | ||
-> m (b, c) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do we feel about the order of type variables in generalBracket
? Our primary motivator for picking this order of arguments was to match what bracket
does, but now we have:
bracket
:: IO a
-> (a -> IO b)
-> (a -> IO c)
-> IO c
generalBracket
:: m a
-> (a -> ExitCase b -> m c)
-> (a -> m b)
-> m (b, c)
Now, the type variables b
and c
are in the opposite order as in bracket
. We could conceivably change this to:
generalBracket
:: m a
-> (a -> ExitCase c -> m b)
-> (a -> m c)
-> m (b, c)
Or even:
generalBracket
:: m a
-> (a -> ExitCase c -> m b)
-> (a -> m c)
-> m (c, b)
I don't have a strong opinion one way or the other. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a strong opinion either. I'll change it to your first suggestion in order to at least match bracket's (a -> m c)
, then change it back if someone feels strongly about this :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. The return type is interesting because it might hint at an order of evaluation. That is, the action which returns c
runs before the action which returns b
, hence m (c, b)
. On the other hand, this puts us back into a state where the type variables are out-of-order again. Perhaps the order of evaluation is already obvious because ExitCase c
appears as an argument to the function which returns an m b
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that I am attempting the change, I feel like changing the variable names is a bad idea. It's important to thread the state in the correct order: from acquire to use, then from use to release. By swapping the order of use
and release
to match bracket
, we have already made it less obvious that this is what implementors need to do. Let's at least keep these variable names as is in order to give a hint of the right ordering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be crazy to change change the type variables in bracket
to match generalBracket
, to make it clearer that even though use
is last so bracket
curries better, it is executed in the middle of the a
and c
bits?
bracket
:: IO a
-> (a -> IO c)
-> (a -> IO b)
-> IO b
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds like a reasonable suggestion too (short of actually swapping the order of those arguments).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
Looking good! At this point, the only issue is that this doesn't build on old GHCs due to import Control.Applicative At around line 106 or so (within the |
to match the order in which the actions will be executed, like in generalBracket.
So close :) Travis is now complaining:
I think replacing these uses of |
Oh, right. Currently, the #if __GLASGOW_HASKELL__ < 710
import Control.Applicative
#endif |
Slowly but surely! |
Thanks, @gelisam! I'll try to get a new release out next week. |
I made the changes discussed in #63, added some tests, updated the documentation, and added a new
onError
method which, unlikeonException
, also runs its handler when the computation aborts which an error which is not an exception.While we're making breaking changes, would it be worthwhile to also generalize the methods in
MonadCatch
so they can act on those non-exception errors?