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.
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.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With