7

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.

Community
  • 1
  • 1
crockeea
  • 21,467
  • 10
  • 44
  • 93
  • 3
    I'm curious why you think the newtype boxes are such an ugly solution? It frankly makes the code easier to read, `PolyModifier` is easier to understand than `forall m. (Mutable v) (PrimState m) r -> m ()`. Other than that, you're giving up some assurances the type systems gives you. It's hard to say whether you have here since, you haven't shown us all the code. But now you're making assertions like "x will never be ever viewed again" since you're just clobbering a pure structure – Daniel Gratzer Nov 14 '13 at 16:43
  • Sure, the signature for *one* function is nicer, but everywhere I call that function, I'll have to `map PM` onto the function list. I also didn't like wrapping something that has no reason to be wrapped from a abstraction perspective: it's just a function, nothing special about it. – crockeea Nov 14 '13 at 16:45
  • I've had some similar situations when dealing with iso-recursive types and you *have* to use a newtype. This is the common practice in lens as well. I'd take this over unsafePeformIO, keystrokes < hours of debugging. – Daniel Gratzer Nov 14 '13 at 16:48
  • @jozefg That's really my question: [why] would there be hours of debugging? I don't understand the differences between how the ST monad would handle the code vs IO. – crockeea Nov 14 '13 at 18:05
  • What goes wrong if you just move the `forall` outside the list, as in `(forall m. [Mutable v (PrimState m) r -> m ()]) -> v r -> v r`? (In other words, are you sure you need to store something polymorphic in your structure, rather than just having a polymorphic structure?) – Daniel Wagner Nov 15 '13 at 02:34
  • @DanielWagner Interesting idea, but GHC still requests `-XImpredicativeTypes`. – crockeea Nov 15 '13 at 03:12
  • @Eric Are you sure? It shouldn't need impredicative types for what I wrote -- and it doesn't ask for them when I try it here. Though it does give other errors. It would help a lot if you included enough code to reproduce your exact problem without guesswork. – Daniel Wagner Nov 15 '13 at 03:35

1 Answers1

5

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

Daniel Wagner
  • 128,625
  • 9
  • 198
  • 347