simonmar / monad-par

124 stars 37 forks source link

Investigate Safe Haskell issues wrt MonadIO instances and run interfaces #18

Open acfoltzer opened 12 years ago

acfoltzer commented 12 years ago

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
rrnewton commented 12 years ago

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

simonmar commented 12 years ago

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 :)

acfoltzer commented 12 years ago

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.

simonmar commented 12 years ago

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!).

rrnewton commented 12 years ago

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.