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