gpt4 book ai didi

haskell - 什么时候unsafeInterleaveIO不安全?

转载 作者:行者123 更新时间:2023-12-03 13:24:36 25 4
gpt4 key购买 nike

与其他不安全*操作不同,unsafeInterleaveIOthe documentation可能存在的陷阱并不十分清楚。那么到底什么时候不安全?我想知道并行/并发和单线程使用的条件。

更具体地说,以下代码中的两个函数在语义上是否等效?如果没有,何时何地?


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的 undefined 状态。惰性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

    这不是我们通常从绑定(bind)通常的懒状态单子(monad)得到的结果。
    假设状态变量 s带有一些实际含义(它没有),则它看起来更像是并发IO(或函数正确表示的交错IO),而不是我们通常用“惰性状态monad”表示的惰性IO。懒惰状态通过关联操作正确地进行了线程化。

    我尝试实现一个真正的惰性IO monad,但很快意识到,为了为IO数据类型定义一个惰性的monadic组成,我们需要能够提升/解除 RealWorld。但是,这似乎是不可能的,因为 State# sRealWorld都没有构造函数。即使有可能,我也不得不代表我们真实世界的精确,功能性代表,这也是不可能的。

    但是我仍然不确定标准的Haskell 2010是否会破坏参照透明性,或者惰性IO本身是否不好。至少似乎完全有可能建立一个RealWorld的小型模型,在该模型上,惰性IO完全安全且可预测。并且可能有足够好的近似值,可以满足许多实际目的,而不会破坏参照透明性。

    最佳答案

    在顶部,您拥有的两个功能始终相同。

    v1 = do !a <- x
    y

    v2 = do !a <- unsafeInterleaveIO x
    y

    请记住, unsafeInterleaveIOIO操作推迟到强制其结果执行之前-但您正在通过使用严格的模式匹配 !a来立即强制执行它,因此该操作完全不会延迟。因此 v1v2完全相同。

    一般来说

    通常,您需要证明自己对 unsafeInterleaveIO的使用是安全的。如果调用 unsafeInterleaveIO x,那么您必须证明可以随时调用 x并仍然产生相同的输出。

    关于惰性IO的现代观点

    ...是懒惰的IO是危险的,并且在99%的时间内都是个坏主意。

    它要解决的主要问题是必须在 IO monad中完成IO,但是您希望能够执行增量IO,并且不想重写所有纯函数来调用IO回调以获取更多数据。增量IO很重要,因为它使用的内存更少,从而可以在不过多更改算法的情况下对不适合内存的数据集进行操作。

    惰性IO的解决方案是在 IO monad之外进行IO。这通常不安全。

    今天,人们正在使用 ConduitPipes之类的库以不同方式解决增量IO问题。与“懒惰” 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将使其正常工作。

    链接

    从Haskell邮件列表中的Oleg Kiselyov的 Lazy IO breaks purity中:

    我们演示了惰性IO如何破坏参照透明性。一个纯
    Int->Int->Int类型的函数给出不同的整数,具体取决于
    根据其论证的评估顺序。我们的Haskell98代码使用
    只不过是标准输入。我们得出结论称赞纯度
    Haskell和广告懒惰IO不一致。

    ...

    懒惰的IO不应该被认为是好的风格。常见的之一
    纯度的定义是,纯表达式应评估为
    相同的结果,与评估顺序无关,或者等于
    代替等于。如果类型为Int的表达式的计算结果为
    1,我们应该能够将每次出现的表达式替换为
    1而不会改变结果和其他可观察的结果。

    从Haskell邮件列表中的Oleg Kiselyov的 Lazy vs correct IO中:

    毕竟,还有什么可以反对
    Haskell的精神比带有观察侧的“纯”函数
    效果。使用惰性IO,确实必须在正确性之间做出选择
    和性能。这样的代码的外观特别奇怪
    在此列表中列出了懒惰IO出现死锁的证据之后
    不到一个月前。更不用说不可预测的资源使用了,
    依赖终结器关闭文件(忘记GHC不会
    确保将最终运行终结器)。

    Kiselyov编写了 Iteratee库,它是惰性IO的第一个真正的替代方案。

    关于haskell - 什么时候unsafeInterleaveIO不安全?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13263692/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com