haskellmonadsimpredicativetypes

Practical Implications of runST vs unsafePerformIO


I want something like

f :: [forall m. (Mutable v) (PrimState m) r -> m ()] -> v r -> v r -- illegal signature
f gs x = runST $ do
  y <- thaw x
  foldM_ (\_ g -> g y) undefined gs -- you get the idea
  unsafeFreeze y

I'm essentially in the same position I was in this question where Vitus commented:

[I]f you want keep polymorphic functions inside some structure, you need either specialized data type (e.g. newtype I = I (forall a. a -> a)) or ImpredicativeTypes.

Also, see this question. The problem is, these are both really ugly solutions. So I've come up with a third alternative, which is to avoid the polymorphism altogether by running what "should" be a ST computation in IO instead. Thus f becomes:

f :: [(Mutable v) RealWorld r -> IO ()] -> v r -> v r
f gs x = unsafePerformIO $ do
  y <- thaw x
  foldM_ (\_ g -> g y) undefined gs -- you get the idea
  unsafeFreeze y

I feel slightly dirty for going the unsafe IO route compared the to the "safe" ST route, but if my alternative is a wrapper or impredicative types... Apparently, I'm not alone.

Is there any reason I shouldn't use unsafePerformIO here? In this case, is it really unsafe at all? Are there performance considerations or anything else I should be aware of?

--------------EDIT----------------

An answer below shows me how to get around this problem altogether, which is great. But I'm still interested in the original question (implicaitons of runST vs unsafePerformIO when using mutable vectors) for educational purposes.


Solution

  • I can't say I understand the problem statement completely yet, but the following file compiles without error under GHC 7.6.2. It has the same body as your first example (and in particular doesn't call unsafePerformIO at all); the primary difference is that the forall is moved outside of all type constructors.

    {-# LANGUAGE RankNTypes #-}
    import Control.Monad
    import Control.Monad.Primitive (PrimState)
    import Control.Monad.ST
    import Data.Vector.Generic hiding (foldM_)
    
    f :: Vector v r => (forall m. [Mutable v (PrimState m) r -> m ()]) -> v r -> v r
    f gs x = runST $ do
      y <- thaw x
      foldM_ (\_ g -> g y) undefined gs
      unsafeFreeze y
    

    Now let's tackle the the ST vs IO question. The reason it's called unsafePerformIO and not unusablePerformIO is because it comes with a proof burden that can't be checked by the compiler: the thing you are running unsafePerformIO on must behave as if it is referentially transparent. Since ST actions come with a (compiler-checked) proof that they behave transparently when executed with runST, this means there is no more danger in using unsafePerformIO on code that would typecheck in ST than there is in using runST.

    BUT: there is danger from a software engineering standpoint. Since the proof is no longer compiler-checked, it's much easier for future refactoring to violate the conditions under which it's safe to use unsafePerformIO. So if it is possible to avoid it (as it seems to be here), you should take efforts to do so. (Additionally, "there is no more danger" doesn't mean "there is no danger": the unsafeFreeze call you are making has its own proof burden that you must satisfy; but then you already had to satisfy that proof burden for the ST code to be correct.)