与其他不安全的*操作不同,unsafeInterleaveIO
的{{3}}对其可能存在的陷阱并不十分清楚。那么到底什么时候不安全?我想知道并行/并发和单线程使用的条件。
更具体地说,以下代码中的两个函数在语义上是等价的吗?如果没有,何时以及如何?
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's和Dietrich的回答以及下面的评论,我简要地浏览了ghc源代码,看看如何在GHC中实现IO monad。在这里,我总结了我的一些发现。
GHC将Haskell实现为一种不纯的,非引用透明的语言。 GHC的运行时通过连续评估具有副作用的不纯函数来运行,就像任何其他函数语言一样。这就是评估订单重要的原因。
unsafeInterleaveIO
是不安全的,因为它可以通过暴露GHC的Haskell的(通常)隐藏的杂质来引入任何类型的并发错误,即使在sigle-threaded程序中也是如此。 (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
这不是我们通常从绑定通常的懒状态monad得到的。
假设状态变量s
具有一些实际意义(它没有),它看起来更像是并发IO (或交错IO ,正如函数所说的那样)而不是 lazy IO ,正如我们通常所说的“懒惰状态monad”,其中尽管存在懒惰状态,但状态通过关联操作正确地进行了线程化。
我试图实现一个真正懒惰的IO monad,但很快意识到为了为IO数据类型定义一个懒惰的monadic组合,我们需要能够提升/解除RealWorld
。但是这似乎是不可能的,因为State# s
和RealWorld
都没有构造函数。即使这是可能的,我也必须代表RealWorld的精确,功能性代表,这也是不可能的。
但是我仍然不确定标准的Haskell 2010是否会破坏引用透明度,或者懒惰的IO本身是不是很糟糕。至少看起来完全有可能构建一个RealWorld的小模型,懒惰的IO是完全安全和可预测的。并且可能存在足够好的近似值,可以在不破坏参照透明度的情况下实现许多实际目的。
答案 0 :(得分:17)
在顶部,您拥有的两个功能始终相同。
v1 = do !a <- x
y
v2 = do !a <- unsafeInterleaveIO x
y
请记住,unsafeInterleaveIO
推迟IO
操作直到其结果被强制 - 但是您通过使用严格模式匹配!a
立即强制它,因此操作不会延迟到所有。因此v1
和v2
完全相同。
通常,您需要证明您对unsafeInterleaveIO
的使用是安全的。如果您致电unsafeInterleaveIO x
,则必须证明可以随时在处调用x
,并且仍会产生相同的输出。
......懒惰IO是危险的,99%的时候都是个坏主意。
它试图解决的主要问题是IO必须在IO
monad中完成,但是你希望能够进行增量IO并且你不想重写所有纯粹的用于调用IO回调以获取更多数据的函数。增量IO非常重要,因为它占用的内存较少,允许您在不改变算法的情况下对不适合内存的数据集进行操作。
Lazy IO的解决方案是在IO
monad之外执行IO。这通常不安全。
今天,人们通过使用Conduit或Pipes等库来以不同方式解决增量IO问题。管道和管道比Lazy IO更具确定性和良好性能,解决了相同的问题,并且不需要不安全的构造。
请记住,unsafeInterleaveIO
实际上只是unsafePerformIO
,而且类型不同。
以下是由于懒惰IO而导致程序崩溃的示例:
rot13 :: Char -> Char
rot13 x
| (x >= 'a' && x <= 'm') || (x >= 'A' && x <= 'M') = toEnum (fromEnum x + 13)
| (x >= 'n' && x <= 'z') || (x >= 'N' && x <= 'Z') = toEnum (fromEnum x - 13)
| otherwise = x
rot13file :: FilePath -> IO ()
rot13file path = do
x <- readFile path
let y = map rot13 x
writeFile path y
main = rot13file "test.txt"
此程序将无效。使用严格的IO替换惰性IO将使其正常工作。
来自Lazy IO breaks purity的Oleg Kiselyov在Haskell邮件列表上:
我们演示了懒惰的IO如何破坏参照透明度。一个纯粹的 类型
Int->Int->Int
的函数根据不同的整数给出 按其评论顺序排列。我们的Haskell98代码使用 只有标准输入。我们得出结论,颂扬纯度 Haskell和广告懒惰的IO是不一致的。...
懒惰IO不应该被认为是好风格。其中一个常见的 纯度的定义是纯粹的表达式应该评估为 无论评估顺序如何,相同的结果,或等于可以 取代等于。如果Int类型的表达式求值为 1,我们应该能够用表达式替换每个出现的表达式 1不改变结果和其他可观察量。
来自Lazy vs correct IO的Oleg Kiselyov在Haskell邮件列表上:
毕竟,还有什么可以反对的 Haskell的精神比具有可观察的一面的“纯粹”功能 效果。使用Lazy IO,确实必须在正确性之间做出选择 和表现。这种代码的出现特别奇怪 在与Lazy IO发生死锁的证据之后,列在此列表中 不到一个月前。更不用说不可预测的资源使用和 依靠终结器来关闭文件(忘记GHC没有 保证终结者将全部运行。
Kiselyov写了Iteratee库,这是懒惰IO的第一个真正的替代品。
答案 1 :(得分:10)
Laziness意味着何时(以及是否)确实实际执行计算取决于运行时实现何时(以及是否)决定它需要该值。作为Haskell程序员,您完全放弃对评估顺序的控制(除了代码中固有的数据依赖性,以及当您开始使用严格性来强制运行时做出某些选择时)。
这对于纯计算非常有用,因为无论何时执行纯计算的结果都是完全相同的(除非您执行实际上不需要的计算,否则可能遇到错误或无法终止,当另一个评估订单可能允许程序成功终止;但任何评估订单计算的所有非底部值都是相同的。)
但是当您编写依赖于IO的代码时,评估顺序很重要。 IO
的重点是提供一种构建计算的机制,其步骤依赖于并影响程序之外的世界,这样做的一个重要部分是这些步骤是明确排序的。使用unsafeInterleaveIO
抛弃显式排序,并放弃对IO
操作何时(以及是否)实际执行到运行时系统的控制。
这对于IO操作通常是不安全的,因为它们的副作用之间可能存在依赖性,这些依赖性无法从程序内部的数据依赖性推断出来。例如,一个IO
操作可能会创建一个包含一些数据的文件,另一个IO
操作可能会读取同一个文件。如果它们都“懒洋洋地”执行,那么只有在需要得到的Haskell值时它们才会运行。尽管如此,创建文件可能是IO ()
,并且()
很可能从不。这可能意味着首先执行读取操作,要么失败要么读取已经在文件中的数据,而不是应该由其他操作放在那里的数据。无法保证运行时系统将以正确的顺序执行它们。要使用始终为IO
执行此操作的系统正确编程,您必须能够准确预测Haskell运行时将选择执行各种{{1}的顺序。行动。
将IO
视为对编译器的承诺(它无法验证,它只会信任您)当unsafeInterlaveIO
操作被执行时无关紧要出来,或者它是否完全被淘汰。这就是所有IO
函数的真实含义;它们提供的设施一般不安全,并且不能自动检查安全性,但在特定情况下可以是安全的。您有责任确保您使用它们实际上是安全的。但是如果你对编译器做出承诺,并且你的承诺是假的,那么就会产生令人不快的错误。名称中的“不安全”是吓唬你思考你的具体情况,并决定你是否真的可以向编译器做出承诺。
答案 2 :(得分:2)
您的joinIO
和joinIO'
不在语义上等效。它们通常是相同的,但有一个微妙的参与:一个爆炸模式使一个值严格,但这就是它的全部。 Bang模式使用seq
实现,并且不强制执行特定的评估顺序,特别是以下两个在语义上等效:
a `seq` b `seq` c
b `seq` a `seq` c
GHC可以在返回c之前评估b或a。实际上,它可以首先评估c,然后评估a和b,然后返回c。或者,如果它可以静态地证明a或b是非底部的,或者c 是底部,则它根本不必评估a或b。一些优化确实可以充分利用这一事实,但在实践中并没有经常出现。
相比之下, unsafeInterleaveIO
对所有或任何这些变化都很敏感 - 它不依赖于某些函数的严格程度的语义属性,而是取决于何时评估某些函数的操作属性。因此,所有上述转换都是可见的,这就是为什么只要感觉合适就可以将unsafeInterleaveIO
视为非确定性地或多或少地执行其IO,这是合理的。
这实质上是unsafeInterleaveIO
不安全的原因 - 它是正常使用中唯一可以检测应该保留意义的转换的机制。这是你能够发现评估的唯一方式,权利应该是不可能的。
顺便说一句,精神上可以将unsafe
添加到GHC.Prim
的每个函数,也可能是其他几个GHC.
模块。他们肯定不是普通的Haskell。
答案 3 :(得分:2)
基本上问题中“更新”下的所有内容都很混乱,甚至没有错,所以当你试图理解我的答案时,请尽量忘记它。
看看这个功能:
badLazyReadlines :: Handle -> IO [String]
badLazyReadlines h = do
l <- unsafeInterleaveIO $ hGetLine h
r <- unsafeInterleaveIO $ badLazyReadlines h
return (l:r)
除了我想要说明的内容之外:上述函数也无法处理到达文件的末尾。但暂时忽略它。
main = do
h <- openFile "example.txt" ReadMode
lns <- badLazyReadlines h
putStrLn $ lns ! 4
这将打印“example.txt”的第一行,因为列表中的第5行实际上是从文件中读取的第一行。