Asynchronous exception wormholes kill modularity
Simon Marlow
marlowsd at gmail.com
Wed Apr 7 11:12:36 EDT 2010
On 25/03/2010 23:16, Bas van Dijk wrote:
> On Thu, Mar 25, 2010 at 11:23 PM, Simon Marlow<marlowsd at gmail.com> wrote:
>>> So I'm all for deprecating 'block' in favor of 'mask'. However what do
>>> we call 'unblock'? 'unmask' maybe? However when we have:
>>>
>>> mask $ mask $ unmask x
>>>
>>> and these operations have the counting nesting levels semantics,
>>> asynchronous exception will not be unmasked in 'x'. However I don't
>>> currently know of a nicer alternative.
>>
>> But that's the semantics you wanted, isn't it? Am I missing something?
>
> Yes I like the nesting semantics that Twan proposed.
>
> But with regard to naming, I think the name 'unmask' is a bit
> misleading because it doesn't unmask asynchronous exceptions. What it
> does is remove a layer of masking so to speak. I think the names of
> the functions should reflect the nesting or stacking behavior. Maybe
> something like:
>
> addMaskingLayer :: IO a -> IO a
> removeMaskingLayer :: IO a -> IO a
> nrOfMaskingLayers :: IO Int
>
> However I do find those a bit long and ugly...
I've been thinking some more about this, and I have a new proposal.
I came to the conclusion that counting nesting layers doesn't solve the
problem: the wormhole still exists in the form of nested unmasks. That
is, a library function could always escape out of a masked context by
writing
unmask $ unmask $ unmask $ ...
enough times.
The functions blockedApply and blockedApply2 proposed by Bas van Dijk
earlier solve this problem:
blockedApply :: IO a -> (IO a -> IO b) -> IO b
blockedApply a f = do
b <- blocked
if b
then f a
else block $ f $ unblock a
blockedApply2 :: (c -> IO a) -> ((c -> IO a) -> IO b) -> IO b
blockedApply2 g f = do
b <- blocked
if b
then f g
else block $ f $ unblock . g
but they are needlessly complicated, in my opinion. This offers the
same functionality:
mask :: ((IO a -> IO a) -> IO b) -> IO b
mask io = do
b <- blocked
if b
then io id
else block $ io unblock
to be used like this:
a `finally` b =
mask $ \restore -> do
r <- restore a `onException` b
b
return r
So the property we want is that if I call a library function
mask $ \_ -> call_library_function
then there's no way that the library function can unmask exceptions. If
all they have access to is 'mask', then that's true.
It's possible to mis-use the API, e.g.
getUnmask = mask return
but this is also possible using blockedApply, it's just a bit harder:
getUnmask = do
m <- newEmptyMVar
f <- blockedApply (join $ takeMVar m) return
return (\io -> putMVar m io >> f)
To prevent these kind of shennanigans would need a parametricity trick
like the ST monad. I don't think it's a big problem that you can do
this, as long as (a) we can explain why it's a bad idea in the docs, and
(b) we can still give a semantics to it, which we can.
So in summary, my proposal for the API is:
mask :: ((IO a -> IO a) -> IO b) -> IO b
-- as above
mask_ :: IO a -> IO a
mask_ io = mask $ \_ -> io
and additionally:
nonInterruptibleMask :: ((IO a -> IO a) -> IO b) -> IO b
nonInterruptibleMask_ :: IO a -> IO a
which is just like mask/mask_, except that blocking operations (e.g.
takeMVar) are not interruptible. Nesting mask inside
nonInterruptibleMask has no effect. The new version of 'blocked' would be:
data MaskingState = Unmasked
| MaskedInterruptible
| MaskedNonInterruptible
getMaskingState :: IO MaskingState
Comments? I have a working implementation, just cleaning it up to make
a patch.
Cheers,
Simon
More information about the Libraries
mailing list