[Haskell-cafe] Re: Asynchronous exception wormholes kill modularity

Simon Marlow marlowsd at gmail.com
Thu Apr 22 04:30:08 EDT 2010


On 21/04/2010 19:38, Bas van Dijk wrote:
> On Tue, Apr 20, 2010 at 12:56 PM, Simon Marlow<marlowsd at gmail.com>  wrote:
>> On 09/04/2010 12:14, Bertram Felgenhauer wrote:
>>> It could be baked into a variant of the forkIO primitive, say
>>>
>>>      forkIOwithUnblock :: ((IO a ->    IO a) ->    IO b) ->    IO ThreadId
>>
>> I agree with the argument here.  However, forkIOWithUnblock reintroduces the
>> "wormhole", which is bad.
>>
>> The existing System.Timeout.timeout does it the other way around: the forked
>> thread sleeps and then sends an exception to the main thread. This version
>> work if exceptions are masked, regardless of whether we have
>> forkIOWithUnblock.
>>
>> Arguably the fact that System.Timeout.timeout uses an exception is a visible
>> part of its implementation: the caller must be prepared for this, so it is
>> not unreasonable for the caller to also ensure that exceptions are unmasked.
>>   But it does mean that a library cannot use System.Timeout.timeout invisibly
>> as part of its implementation.  If we had forkIOWithUnblock that would solve
>> this case too, as the library code can use a private thread in which
>> exceptions are unmasked.  This is quite a nice solution too, since a private
>> ThreadId is not visible to anyone else and hence cannot be the target of any
>> unexpected exceptions.
>>
>> So I think I'm convinced that forkIOWithUnblock is necessary.  It's a shame
>> that it can be misused, but I don't see a way to avoid that.
>>
>> Cheers,
>>         Simon
>>
>
> I can see how forkIOWithUnblock (or forkIOWithUnnmask) can introduce a wormhole:
>
> unmaskHack1 :: IO a ->  IO a
> unmaskHack1 m = do
>    mv<- newEmptyMVar
>    tid<- forkIOWithUnmask $ \unmask ->  putMVar mv unmask
>    unmask<- takeMVar mv
>    unmask m
>
> We can try to solve it using a trick similar to the ST monad:

Funnily enough, before posting the above message I followed exactly the 
line of reasoning you detail below to discover that there isn't a way to 
fix this using parametricity.  It's useful to have it documented, though 
- thanks.

Cheers,
	Simon



> {-# LANGUAGE Rank2Types #-}
>
> import qualified Control.Exception as Internal (unblock)
> import Control.Concurrent (forkIO, ThreadId)
> import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
>
> newtype Unmask s = Unmask (forall a. IO a ->  IO a)
>
> forkIOWithUnmask :: (forall s. Unmask s ->  IO ()) ->  IO ThreadId
> forkIOWithUnmask f = forkIO $ f $ Unmask Internal.unblock
>
> apply :: Unmask s ->  IO a ->  IO a
> apply (Unmask f) m = f m
>
> thisShouldWork = forkIOWithUnmask $ \unmask ->  apply unmask (return ())
>
> The following shouldn't work and doesn't because we get the following
> type error:
>
> "Inferred type is less polymorphic than expected. Quantified type
> variable `s' is mentioned in the environment."
>
> unmaskHack2 :: IO a ->  IO a
> unmaskHack2 m = do
>    mv<- newEmptyMVar
>    tid<- forkIOWithUnmask $ \unmask ->  putMVar mv unmask
>    unmask<- takeMVar mv
>    apply unmask m
>
> However we can still hack the system by not returning the 'Unmask s'
> but returning the IO computation 'apply unmask m' as in:
>
> unmaskHack3 :: IO a ->  IO a
> unmaskHack3 m = do
>    mv<- newEmptyMVar
>    tid<- forkIOWithUnmask $ \unmask ->  putMVar mv (apply unmask m)
>    unmaskedM<- takeMVar mv
>    unmaskedM -- (or use join)
>
> AFAIK the only way to solve the latter is to also parametrize IO with s:
>
> data IO s a = ...
>
> newtype Unmask s = Unmask (forall s2 a. IO s2 a ->  IO s2 a)
>
> forkIOWithUnmask :: (forall s. Unmask s ->  IO s ()) ->  IO s2 ThreadId
> forkIOWithUnmask f = forkIO $ f $ Unmask Internal.unblock
>
> apply :: Unmask s ->  IO s2 a ->  IO s a
> apply (Unmask f) m = f m
>
> With this unmaskHack3 will give the desired type error.
>
> Of course parameterizing IO with s is a radical change that will break
> _a lot of_ code. However besides solving the latter problem the extra
> s in IO also create new opportunities. Because all the advantages of
> ST can now also be applied to IO. For example we can have:
>
> scope :: (forall s. IO s a) ->  IO s2 a
>
> data LocalIORef s a
>
> newLocalIORef :: a ->  IO s (LocalIORef s a)
> readLocalIORef :: LocalIORef s a ->  IO s a
> writeLocalIORef :: LocalIORef s a ->  a ->  IO s a
>
> regards,
>
> Bas



More information about the Haskell-Cafe mailing list