simonmar / monad-par

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Investigate Safe Haskell issues wrt MonadIO instances and run interfaces

acfoltzer opened this issue · comments

From a Safe Haskell perspective, the following combination is safe:

instance MonadIO Par
runParIO :: Par a -> IO a

However, if this instance exists alongside runPar :: Par a -> a, safety is gone.

A brute-force solution might involve a triangular module structure along with generous amounts of newtype deriving:

module SMP.Internal (Par, runPar, runParIO) where
newtype Par a = ...
  deriving (MonadIO ...)
runPar = ...
runParIO = ...
{-# LANGUAGE Safe #-}
module SMP.IO (Par, runParIO) where
-- use Par and runParIO from SMP.Internal
{-# LANGUAGE Safe #-}
module SMP (Par, runPar) where
newtype Par a = Par (SMP.Internal.Par a)
  deriving ({- not MonadIO -})
runPar = SMP.Internal.runPar . unPar

I don't presently see a better solution than this, though certainly we'd like to scrap that boilerplate if we could.

What safety issue are you referring to here? The problem with returning IVars from a Par computation?

I had a suggestion from Nick Smallbone recently that appears to solve this problem in an relatively unintrusive way, but it's a bit delicate. The idea is to give runPar this type:

runPar :: Typeable a => Par a -> a

Now, with this type it seems to be impossible to use runPar in such a way that the type system is subverted. I challenge you to try :)

For this ticket, we are actually referring to safety in the Safe Haskell sense; with Par exported as a MonadIO instance, runPar amounts to unsafePerformIO. However, we want to keep the MonadIO instance out there so that meta-scheduler resources can use it for implementation purposes, but still export a Safe Haskell interface to end users.

Regarding IVar escape, this seems to do the trick, no?

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE StandaloneDeriving #-}

import Control.Monad.Par.Class
import Control.Monad.Par.Meta.SMP

import Data.Typeable

deriving instance Typeable1 IVar

runPar' :: Typeable a => Par a -> a
runPar' = runPar

uhoh = let escapee :: IVar Int
           escapee = runPar' $ do iv <- new :: Par (IVar Int)
                                  fork (get iv >> return ())
                                  return iv
       in runPar' (put escapee 5)

This example currently blows up meta-par, so it might be a good case for debugging. With a nested scheduler, it seems like this ought to work, even if it's semantically rather nonsensical.

I agree that a MonadIO instance is unsafe, but I'll take your word for it that you want it anyway...

Re your example, you can only cause a runtime crash if you can make a polymorphic IVar escape from a Par computation, because then you can write it at one type and read it at another. The Typeable constraint prevents that from happening. I believe the example you gave still has deterministic behaviour (although specifying what behaviour it actually has might be tricky!).

As to the question of WHY we want IO --

Basically we are finding scenarios where we do want to write effectful parallel programs, but would still prefer the lighter-weight monad-par scheduling to forkIO+MVars. For example, we are looking into implementing certain distributed web services using meta/monad-par.

My original idea for this is insufficient. I separated the unsafe liftIO equivalent into a module that is marked as unsafe:

Control.Monad.Par     -- provides concrete API functions, Safe
Control.Monad.Par.Class -- provides overloaded API functions, Safe
Control.Monad.Par.Unsafe -- provides ParUnsafe class with liftIO equivalent, UNSAFE

This allows pure monad-par use, or being naughty, but the latter disables safe haskell.

Yet that shouldn't be necessary, {liftIO, runParIO} is a valid, Safe-Haskell set of operations. Hence Adam's proposal for the trio of modules above and the newtype that would make sure that the programmer who uses liftIO can't also use run.