与其他 unsafe* 操作不同,文档对于unsafeInterleaveIO
它可能存在的陷阱不是很清楚。那么究竟什么时候不安全呢?我想知道并行/并发和单线程使用的条件。
更具体地说,以下代码中的两个函数在语义上是否等效?如果没有,何时以及如何?
joinIO :: IO a -> (a -> IO b) -> IO b
joinIO a f = do !x <- a
!x' <- f x
return x'
joinIO':: IO a -> (a -> IO b) -> IO b
joinIO' a f = do !x <- unsafeInterleaveIO a
!x' <- unsafeInterleaveIO $ f x
return x'
以下是我在实践中将如何使用它:
data LIO a = LIO {runLIO :: IO a}
instance Functor LIO where
fmap f (LIO a) = LIO (fmap f a)
instance Monad LIO where
return x = LIO $ return x
a >>= f = LIO $ lazily a >>= lazily . f
where
lazily = unsafeInterleaveIO . runLIO
iterateLIO :: (a -> LIO a) -> a -> LIO [a]
iterateLIO f x = do
x' <- f x
xs <- iterateLIO f x' -- IO monad would diverge here
return $ x:xs
limitLIO :: (a -> LIO a) -> a -> (a -> a -> Bool) -> LIO a
limitLIO f a converged = do
xs <- iterateLIO f a
return . snd . head . filter (uncurry converged) $ zip xs (tail xs)
root2 = runLIO $ limitLIO newtonLIO 1 converged
where
newtonLIO x = do () <- LIO $ print x
LIO $ print "lazy io"
return $ x - f x / f' x
f x = x^2 -2
f' x = 2 * x
converged x x' = abs (x-x') < 1E-15
虽然我宁愿避免在严肃的应用程序中使用这段代码,因为这太可怕了unsafe*
东西,我至少可以比更严格的 IO monad 在决定“收敛”的含义时更懒惰,导致(我认为是)更惯用的 Haskell。这带来了另一个问题:为什么它不是 Haskell(或 GHC?)IO monad 的默认语义?我听说过一些惰性 IO 的资源管理问题(GHC 仅通过一小部分固定命令提供),但通常给出的示例有点类似于损坏的 makefile:资源 X 依赖于资源 Y,但如果你失败了要指定依赖关系,您会得到 X 的未定义状态。惰性 IO 真的是这个问题的罪魁祸首吗?(另一方面,如果上述代码中存在细微的并发错误,例如死锁,我会将其视为更根本的问题。)
更新
阅读 Ben 和 Dietrich 的回答以及他在下面的评论,我简要浏览了 ghc 源代码以了解 IO monad 是如何在 GHC 中实现的。在这里,我总结了我的一些发现。
GHC 将 Haskell 实现为一种不纯的、非引用透明的语言。GHC 的运行时通过连续评估具有副作用的不纯函数来运行,就像任何其他函数式语言一样。这就是评估顺序很重要的原因。
unsafeInterleaveIO
是不安全的,因为即使在单线程程序中,它也会通过暴露 GHC 的 Haskell 的(通常)隐藏杂质来引入任何类型的并发错误。(iteratee
这似乎是一个不错且优雅的解决方案,我一定会学习如何使用它。)IO monad 必须是严格的,因为安全、惰性的 IO monad 需要对 RealWorld 的精确(提升)表示,这似乎是不可能的。
不安全的不仅仅是 IO monad 和
unsafe
函数。整个 Haskell(由 GHC 实现)可能是不安全的,并且(GHC 的)Haskell 中的“纯”功能只是按照惯例和人们的善意纯粹的。类型永远不能证明纯度。
为了看到这一点,我演示了 GHC 的 Haskell 如何不具有引用透明性,而不管 IO monad、unsafe*
函数等。
-- An evil example of a function whose result depends on a particular
-- evaluation order without reference to unsafe* functions or even
-- the IO monad.
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}
{-# LANGUAGE BangPatterns #-}
import GHC.Prim
f :: Int -> Int
f x = let v = myVar 1
-- removing the strictness in the following changes the result
!x' = h v x
in g v x'
g :: MutVar# RealWorld Int -> Int -> Int
g v x = let !y = addMyVar v 1
in x * y
h :: MutVar# RealWorld Int -> Int -> Int
h v x = let !y = readMyVar v
in x + y
myVar :: Int -> MutVar# (RealWorld) Int
myVar x =
case newMutVar# x realWorld# of
(# _ , v #) -> v
readMyVar :: MutVar# (RealWorld) Int -> Int
readMyVar v =
case readMutVar# v realWorld# of
(# _ , x #) -> x
addMyVar :: MutVar# (RealWorld) Int -> Int -> Int
addMyVar v x =
case readMutVar# v realWorld# of
(# s , y #) ->
case writeMutVar# v (x+y) s of
s' -> x + y
main = print $ f 1
为了方便参考,我收集了一些 GHC 实现的 IO monad 的相关定义。(以下所有路径都是相对于 ghc 源存储库的顶级目录。)
-- Firstly, according to "libraries/base/GHC/IO.hs",
{-
The IO Monad is just an instance of the ST monad, where the state is
the real world. We use the exception mechanism (in GHC.Exception) to
implement IO exceptions.
...
-}
-- And indeed in "libraries/ghc-prim/GHC/Types.hs", We have
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
-- And in "libraries/base/GHC/Base.lhs", we have the Monad instance for IO:
data RealWorld
instance Functor IO where
fmap f x = x >>= (return . f)
instance Monad IO where
m >> k = m >>= \ _ -> k
return = returnIO
(>>=) = bindIO
fail s = failIO s
returnIO :: a -> IO a
returnIO x = IO $ \ s -> (# s, x #)
bindIO :: IO a -> (a -> IO b) -> IO b
bindIO (IO m) k = IO $ \ s -> case m s of (# new_s, a #) -> unIO (k a) new_s
unIO :: IO a -> (State# RealWorld -> (# State# RealWorld, a #))
unIO (IO a) = a
-- Many of the unsafe* functions are defined in "libraries/base/GHC/IO.hs":
unsafePerformIO :: IO a -> a
unsafePerformIO m = unsafeDupablePerformIO (noDuplicate >> m)
unsafeDupablePerformIO :: IO a -> a
unsafeDupablePerformIO (IO m) = lazy (case m realWorld# of (# _, r #) -> r)
unsafeInterleaveIO :: IO a -> IO a
unsafeInterleaveIO m = unsafeDupableInterleaveIO (noDuplicate >> m)
unsafeDupableInterleaveIO :: IO a -> IO a
unsafeDupableInterleaveIO (IO m)
= IO ( \ s -> let
r = case m s of (# _, res #) -> res
in
(# s, r #))
noDuplicate :: IO ()
noDuplicate = IO $ \s -> case noDuplicate# s of s' -> (# s', () #)
-- The auto-generated file "libraries/ghc-prim/dist-install/build/autogen/GHC/Prim.hs"
-- list types of all the primitive impure functions. For example,
data MutVar# s a
data State# s
newMutVar# :: a -> State# s -> (# State# s,MutVar# s a #)
-- The actual implementations are found in "rts/PrimOps.cmm".
因此,例如,忽略构造函数并假设引用透明,我们有
unsafeDupableInterleaveIO m >>= f
==> (let u = unsafeDupableInterleaveIO)
u m >>= f
==> (definition of (>>=) and ignore the constructor)
\s -> case u m s of
(# s',a' #) -> f a' s'
==> (definition of u and let snd# x = case x of (# _,r #) -> r)
\s -> case (let r = snd# (m s)
in (# s,r #)
) of
(# s',a' #) -> f a' s'
==>
\s -> let r = snd# (m s)
in
case (# s, r #) of
(# s', a' #) -> f a' s'
==>
\s -> f (snd# (m s)) s
这不是我们通常从绑定通常的惰性状态单子中得到的。假设状态变量s
带有一些真正的含义(它没有),它看起来更像是一个并发 IO(或函数正确说的交错 IO )而不是我们通常所说的“惰性状态单子”的惰性 IO ,其中尽管懒惰状态通过关联操作适当地线程化。
我试图实现一个真正惰性的 IO monad,但很快意识到为了为 IO 数据类型定义一个惰性 monadic 组合,我们需要能够提升/取消提升RealWorld
. 然而,这似乎是不可能的,因为State# s
和都没有构造函数RealWorld
。即使这是可能的,我也必须代表我们真实世界的精确、功能表示,这也是不可能的。
但我仍然不确定标准的 Haskell 2010 是否破坏了引用透明度或惰性 IO 本身是坏的。至少似乎完全有可能构建一个 RealWorld 的小型模型,在该模型上,惰性 IO 是完全安全和可预测的。并且可能有一个足够好的近似值,可以在不破坏引用透明度的情况下服务于许多实际目的。