与其他不安全*操作不同,文档 http://hackage.haskell.org/packages/archive/base/latest/doc/html/System-IO-Unsafe.html#v:unsafeInterleaveIO for 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 真的是这个问题的罪魁祸首吗? (另一方面,如果上述代码中存在微妙的并发错误(例如死锁),我会将其视为更根本的问题。)
Update
阅读了 Ben 和 Dietrich 的回答以及他下面的评论,我简要浏览了 ghc 源代码,了解 IO monad 在 GHC 中是如何实现的。在这里,我总结了我的一些发现。
GHC 将 Haskell 实现为一种不纯粹的、非引用透明的语言。 GHC 的运行时通过连续评估具有副作用的不纯函数来运行,就像任何其他函数式语言一样。这就是评估顺序很重要的原因。
unsafeInterleaveIO
是不安全的,因为即使在单线程程序中,它也会通过暴露 GHC Haskell 的(通常)隐藏杂质而引入任何类型的并发错误。 (iteratee
似乎是一个很好且优雅的解决方案,我一定会学习如何使用它。)
IO monad 必须严格,因为安全、惰性的 IO monad 需要真实世界的精确(提升)表示,这似乎是不可能的。
这不仅仅是 IO monadunsafe
不安全的函数。整个 Haskell(由 GHC 实现)可能是不安全的,并且(GHC 的)Haskell 中的“纯”函数只是按照惯例和人们的善意而纯的。类型永远不能证明纯度。
为了看到这一点,我演示了 GHC 的 Haskell 是如何not引用透明,无论 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 (or 交错IO正如函数正确所说)比lazy IO正如我们通常所说的“惰性状态单子”,其中尽管有惰性,但状态仍通过关联操作正确地线程化。
我尝试实现一个真正的惰性 IO monad,但很快意识到,为了定义 IO 数据类型的惰性 monadic 组合,我们需要能够提升/取消提升RealWorld
。但这似乎是不可能的,因为两者都没有构造函数State# s
and RealWorld
。即使这是可能的,我也必须精确地、功能性地表达我们的现实世界,这也是不可能的。
但我仍然不确定标准 Haskell 2010 是否破坏了引用透明度,或者惰性 IO 本身就不好。至少看起来完全有可能构建一个真实世界的小型模型,在该模型上惰性 IO 是完全安全且可预测的。并且可能存在一个足够好的近似值,可以在不破坏引用透明度的情况下满足许多实际目的。