
Hello Mikhail, Sorry, long email. tl;dr I think it makes more sense for throw/catch/mask to be bundled together, and it is the case that splitting these up doesn't address the original issue monad-control was designed to solve. ~ * ~ Anders and I thought a little more about your example, and we first wanted to clarify which instance you thought was impossible to write. For example, we think it should be possible to write: instance MonadBaseControl AIO AIO Notice that the base monad is AIO: this lets you lift arbitrary AIO operations to a transformed AIO monad (e.g. ReaderT r AIO), but no more. If this is the instance you claimed was impossible, we'd like to try implementing it. Can you publish the full example code somewhere? However, we don't think it will be possible to write: instance MonadBaseControl IO AIO Because this lets you leak arbitrary IO control flow into AIO (e.g. forkIO, with both threads having the ability to run the current AIO context), and as you stated, you only want to allow a limited subset of control flow in. (I think this was the intent of the original message.) Maybe client code doesn't want to be attached to AIO base monads, though; that's too restrictive for them. So they'd like to generalize a bit. So let's move on to the issue of your typeclass decomposition. ~ * ~ I don't think it makes too much sense have thing pick off a menu of Abort/Recover/Finally from a semantics perspective:
It's easy to imagine monads that have an instance of one of the classes but not of the others....
I'd like to see some examples. I hypothesize that most of such monads are incoherent, semantically speaking. For example, what does it mean to have a monad that can recover exceptions, but for which you can't throw exceptions? There only a few options: - You have special primitives which throw exceptions, distinct from Haskell's IO exceptions. In that case, you've implemented your own homebrew exception system, and all you get is a 'Catch MyException' which is too specific for a client who is expecting to be able to catch SomeExceptions. - You execute arbitrary IO and allow those exceptions to be caught. But then I can implement Throw: I just embed an IO action that is throwing an exception. - You only execute a limited subset of IO, but when they throw exceptions they throw ordinary IO exceptions. In this case, the client doesn't have access to any scarce resources except the ones you provided, so there's no reason for him to even need this functionality, unless he's specifically coding against your monad. What does it mean to not have a Finally instance, but a Recover and Throw instance? Well, I can manually reimplement finally in this case (with or without support for asynchronous exceptions, depending on whether or not Mask is available): this is how the paper does it (finally is not a primitive.) What does it mean to have a monad that can throw exceptions, but not catch them? This is any of the usual monads that can fail, of which we have many. And of course, you can't allow this in the presence of scarce resources since there is no way to properly deallocate them when exceptions are thrown. So it seems this is just ordinary failure which cannot be used in the presence of arbitrary IO. What does it mean to have all of the above, but not to have a mask instance? One approach is to pretend asynchronous exceptions do not exist. As you do in your example, we can simply mask. I think this is a bit to give up, but I'll concede it. However, I don't think it's acceptable not to provide mask functionality, not mask your interpreter, and allow arbitrary IO. It's now impossible to properly implement many patterns without having subtle race conditions. So it seems we should collapse these into one class, which conveniently maps straight to the semantics defined in "Asynchronous Exceptions in Haskell". class MonadAsyncExc m where mask :: ((forall a. m a -> m a) -> m b) -> m b throw :: SomeException -> m () catch :: m a -> (SomeException -> m a) -> m a But you get to have your cake and eat it too: if you define a monad which is guaranteed to be run with asynchronous exceptions masked, you can define the 'mask' function to be a no-op and not violate any laws! Hooray! But this is in fact what MonadCatchIO was, except that MonadCatchIO was formulated when we still had block/unblock and it required MonadIO. So a useful endeavour would be to punt the MonadIO superclass constraint and fix the definitions, and we have something that is usable to your case. ~ * ~ To contextualize this whole discussion, recall the insiduous problem that *motivated* the creation of monad-control. Suppose that we've done all of the hard work and lifted all of the Control.Exception functions to our new formula (maybe we also need uninterruptibleMask in our class, but no big matter.) Now a user comes along and asks for 'alloca :: Storable a => (Ptr a -> IO b) -> IO b'. Crap! We need to redefine alloca to work for our new monad. So in comes the class Alloca. But there are a billion, billion of these functions. If only there was a way of automatically getting lifted versions of them... and this leads us back to MonadMorphIO, monad-peel, and finally monad-control. This gist of your objection to this typeclass is that there is no principled way to tell the difference between: forkIO :: IO a -> IO a and bracketed :: IO a -> IO a Thus, it is impossible, with the type system given to us by IO, to write a function that will lift bracketed automatically, but not also lift forkIO. Long live the IO sin bin. So the best we can hope for is manually lifting every function we think is safe. You've attempted to work around this by defining a typeclass per function, but as I've tried to argue in this email that there is no way to reasonably reason about these functions in isolation. I also hope that I've convinced you that even with all of these typeclasses, we still only have a partial solution. I think one way forward may be to create a mechanism by which we can identify functions which don't alter control flow, and cheaply lift them via a monad-control like mechanism, while not publishing that mechanism itself (except, maybe, as an unsafe combinator). But this is a discussion that independent of your rant. Cheers, Edward