runST和unsafePerformIO的实际应用意义

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

我基本上处于与Vitus评论的这个问题相同的位置:

如果你想在某个结构内保留多态函数,则需要使用特殊的数据类型(例如,新类型I = I(forall a。a -> a))或ImpredicativeTypes。

此外,请参见该问题。问题是,这些解决方案都非常丑陋。因此,我想出了第三种选择,即通过在IO中运行“应该”是ST计算来完全避免多态性。因此,f变为:

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

我选择使用 unsafeIO 而不是“安全”的 ST,虽然这让我感觉有点不太好,但如果我的替代方案是使用包装器或不确定类型...显然,我并不孤单。

在这里使用 unsafePerformIO 会有什么问题吗? 在这种情况下,它真的很不安全吗? 我需要注意哪些性能方面的问题或其他方面的问题吗?

--------------编辑----------------

下面的答案向我展示了如何完全解决这个问题,这非常好。 但出于教育目的,我仍然对原始问题(使用可变向量时的runSTunsafePerformIO的影响)感兴趣。


3
我很好奇为什么你认为newtype箱是一个如此丑陋的解决方案?实际上,它使得代码更易于阅读,PolyModifierforall m. (Mutable v) (PrimState m) r -> m ()更容易理解。除此之外,你会放弃一些类型系统给你的保证。很难说你在这里放弃了什么,因为你没有展示给我们所有的代码。但现在你正在做出像“x将永远不会再次被查看”这样的断言,因为你只是破坏了一个纯结构。 - daniel gratzer
@jozefg 这确实是我的问题:为什么会有几个小时的调试时间?我不理解ST monad如何处理代码与IO的区别。 - crockeea
如果你只是将 forall 移到列表外面,例如 (forall m. [Mutable v (PrimState m) r -> m ()]) -> v r -> v r,会出现什么问题?(换句话说,你确定需要在结构中存储多态的内容,而不是拥有一个多态的结构吗?) - Daniel Wagner
@DanielWagner 有趣的想法,但 GHC 仍然需要 -XImpredicativeTypes - crockeea
@Eric 你确定吗?根据我写的内容,它不应该需要使用impredicative类型--而且当我在这里尝试时,它也没有要求。虽然它确实会给出其他错误。如果您提供足够的代码以便无需猜测就能重现您的确切问题,那将非常有帮助。 - Daniel Wagner
显示剩余2条评论
1个回答

5

我不能说我完全理解这个问题陈述,但是在 GHC 7.6.2 下,以下文件可以编译通过而没有错误。它具有与您的第一个示例相同的主体(特别是根本不调用 unsafePerformIO); 主要区别在于 forall 移到了所有类型构造函数之外。

{-# 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

现在让我们来解决 ST vs IO 的问题。之所以称其为 unsafePerformIO 而不是 unusablePerformIO,是因为它带有一个证明负担,这个证明负担无法由编译器检查:你在 unsafePerformIO 上运行的东西必须表现得像引用透明一样。由于当使用 runST 执行时,ST 操作会附带(由编译器检查的)证明,这意味着在使用能够通过类型检查的 ST 代码中使用 unsafePerformIO 不会比使用 runST 更加危险。
但是:从软件工程的角度来看,存在危险。由于该证明不再受编译器检查,未来重构可能更容易违反安全使用 unsafePerformIO 的条件。因此,如果可以避免使用它(似乎在这里是可以避免的),应该尽力去做。(此外,“没有更大的危险”并不意味着“没有危险”:你正在进行的 unsafeFreeze 调用有自己的证明负担,你必须满足这个证明负担,但是为了使 ST 代码正确,你已经必须满足这个证明负担。)

我明白我做错了什么:我有一些限制(比如PrimMonad m),但是当我提取出forall m时,我把它们留在了列表内部。谢谢你! - crockeea
我仍然对runSTunsafePerformIO感兴趣,但现在只是出于教育目的。 - crockeea
@Eric 好的,我已经添加了一个关于这个的简短注释。 - Daniel Wagner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接