Practical Implications of runST vs unsafePerformIO
Asked Answered
K

1

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.

Kauppi answered 14/11, 2013 at 15:56 Comment(7)
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 structureSuperimposed
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.Kauppi
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.Superimposed
@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.Kauppi
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?)Lyssa
@DanielWagner Interesting idea, but GHC still requests -XImpredicativeTypes.Kauppi
@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.Lyssa
L
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.)

Lyssa answered 15/11, 2013 at 3:41 Comment(3)
I see what I was doing wrong: I have constraints (like PrimMonad m), but left them inside the list when I pulled out the forall m. Thanks for this!Kauppi
I'm still interested in runST vs unsafePerformIO, just for educational purposes at this point.Kauppi
@Eric Okay, I added a short comment about this.Lyssa

© 2022 - 2024 — McMap. All rights reserved.