MonadThrow/MonadCatch/MonadMask are always available
qrpnxz opened this issue ยท 8 comments
I find it surprising there is no IOE :> es
constraint on the MonadThrow
et al. instances for Eff
. I shouldn't get exceptions from pure computations. Please add IOE :> es
constraint. Those who want these kinds of surprise exceptions can already use throw
from Control.Exception
. Furthermore, MonadCatch
can catch exceptions from Effectful.Error.Static
, which is also not great.
I shouldn't get exceptions from pure computations
You can't prevent that because Haskell is a language where any expression is free to have a throw
inside, not to mention asynchronous exceptions.
Those who want these kinds of surprise exceptions can already use throw from
Control.Exception
.
Exactly, that's why adding an IOE
requirement to MonadThrow
changes nothing, because you can always use throw
.
Furthermore, MonadCatch can catch exceptions from Effectful.Error.Static, which is also not great.
Yes, that is quite unfortunate, but it's only a problem if you catch SomeException
and ignore it, which you should never do.
To summarize:
MonadMask
can't requireIOE
because it allows usage of functions likebracket
andfinally
to perform cleanup actions (like restoring a state on error), even withinrunPureEff
.MonadThrow
is inconsequential because anything can throw at any time.MonadCatch
is the one that would be nice to requireIOE
, but it can't because if it did,MonadMask
would also have to because of arguably invalid class hierarchy in theexceptions
package and that ship unfortunately has sailed.
Good points, in particular for MonadThrow
, you have changed my mind.
Regarding your first and last points, consider this: If the finalization you want to do is for an effect that you run entirely within the catch
, then whatever thing you did in finalization will be irrelevant because the exception threw the context away. If it's dependent on an effect that is also in the stack that's doing the catching (and in particular, it's stateful: IOE
, State
), then it makes sense, because that context will still be relevant after catching. Because of that, I it doesn't seem to be a real problem that you cannot bracket
if you cannot catch
.
I hadn't meant to post just yet. There is of course one effect that is relevant even if you never catch: IOE
(because thankfully exceptions don't delete the universe ๐). If you have IOE
you should absolutely be able to bracket
, which is the case even if MonadMask
requires IOE
, so no problem there. As far as I can tell that is the only exception (haha), so it shouldn't be a problem to have the constraint.
I find it hard to visualize what you described, but if MonadMask
required IOE
, the following function
transactionally :: forall s es a. State s :> es => Eff es a -> Eff es a
transactionally = bracketOnError (get @s) (put @s) . const
cannot be written.
Moreoever, even if your effect stack has IOE
, ideally you want to not have it visible in most of your application code and only use more restricted effects. Again, if MonadMask
required IOE
, you couldn't use things like bracket
in your functions without bringing in the whole IOE
, which would be bad.
I agree IOE
is overkill for a function like transactionally
. So let's write an Effect
then ๐. I'll call it Catch
for now. Catch
will let you catch any (synchronous) IO
exception, and another effect Mask
will let you do masking. Then, looking at the new function type, the possible effects would amount to State s
, "can catch any exception" and "can mask", which are indeed the only effects done by transactionally
.
You've mentioned it would be nice to have MonadCatch
require IOE
. I'm guessing you would find Catch
nice for similar reasons. Here is one motivation: if I pass a throwing higher order effect, and my exceptions disappear, I can find what functions might be responsible; namely those with Catch
or IOE
. Functions without either are transparent to exceptions.
We could also have another effect for catching only exceptions of a particular type.
More concretely why MonadCatch => MonadMask
makes sense.
Let's suppose that the higher order effect in transactionally
throws. If the exception is never caught, or is caught in a context in which State s
has already been run, then the (put @s)
finalization will be irrelevant, because that state will be gone by the time we get control again:
main = neverCatches (transactionally ...) -- Program exits. Bracketing useless.
catch (runState (neverCatches (transactionally ...)))
(\e -> ...) -- State inaccessible. Bracketing useless.
runState $ catch (transactionally ...)
(\e -> ...) -- Still using that state. Bracketing helped.
Now let's say the function using transactionally
doesn't have a Catch
constraint. Then using transactionally
will be disallowed, but that's okay because unless we catch, or run in a context where catching is possible, the effects of the bracket will not be appreciable. Furthermore, the implementation of bracket
actually requires catching, so it makes sense that that constraint would be required.
Now, if the effect is IOE
, then it's a little different:
main = neverCatches (bracket allocRealWorldResource
freeRealWorldResource
badAction)
-- Real world still exists, bracket action still relevant.
But this bracket doesn't need you to say we can Catch
, because with IOE
we get MonadUnliftIO
. So in the one effect you might want to bracket without being able to catch, you can! (Though of course, IOE
can catch too. ๐)
Similarly with mask
, the corruption that mask
would prevent is only relevant if we ever catch, enabling access to our state again. However, it is true that masking itself doesn't technically require the capability to catch
, so having an Effect
just for masking might be nice. Then a function that just masks doesn't look like it might catch when it asserts there is catching in the effect stack. MonadMask
still needs Catch
because it provides a bracket. But if for some reason you only wanted to mask, you could use the Mask
effect.
I'm willing and able to implement these effects.
Please also see #65 (comment).
I pondered the idea of having an Except
effect and constraint these instances to this effect, but discarded the idea precisely because exceptions are everywhere in Haskell.
However, it is true that masking itself doesn't technically require the capability to catch, so having an Effect just for masking might be nice
That would be nice, but it's not possible because of the superclass hierarchy. Try it yourself. It would have to be:
instance Catch :> es => MonadCatch (Eff es)
instance (Catch :> es, Mask :> es) => MonadMask (Eff es)
if you attempt to do this:
instance Catch :> es => MonadCatch (Eff es)
instance Mask :> es => MonadMask (Eff es)
you'll get an error
โข Could not deduce (Catch :> es)
arising from the superclasses of an instance declaration
from the context: Mask :> es
...
if I pass a throwing higher order effect, and my exceptions disappear, I can find what functions might be responsible; namely those with Catch or IOE. Functions without either are transparent to exceptions.
That's a good argument, but we're unfortunately tied by the hierarchy of these classes.
Now let's say the function using transactionally doesn't have a Catch constraint. Then using transactionally will be disallowed, but that's okay because unless we catch, or run in a context where catching is possible, the effects of the bracket will not be appreciable. Furthermore, the implementation of bracket actually requires catching, so it makes sense that that constraint would be required.
The problem is that bracket
catching exceptions is an implementation detail ๐ The caller of bracket
doesn't care how it works, so I find requiring a Catch
effect for it at the call site to be weird.
I'll close this for now. There is no good solution to this problem because of the unfortunate class hierarchy in exceptions
, but I consider the current one the most pragmatic.