什么时候不安全InterleaveIO不安全?

与其他不安全*操作不同,不安全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's(或GHC's?)IO monad的默认语义? 我听说过惰性IO的某些资源管理问题(GHC只提供一小组固定的命令),但这些例子通常有点类似于破解的makefile:资源X依赖于资源Y,但如果您失败要指定依赖关系,你会得到一个未定义的状态。懒惰IO真的是这个问题的罪魁祸首吗? (另一方面,如果在上面的代码中存在一个微妙的并发错误,比如死锁,我会把它作为一个更基本的问题。)

更新

在阅读Ben和Dietrich的回答以及他的评论后,我简要浏览了ghc源代码,以了解如何在GHC中实现IO monad。 在这里,我总结了我的一些发现。

  • 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
    

    这不是我们通常从绑定通常懒惰状态monads得到的。 假设状态变量s带有一些真正的含义(它没有),它看起来更像是一个并发IO(或者函数正确表示的交错IO),而不是像懒惰状态monad那样的惰性IO,其中尽管各州通过联合行动正确地贯彻懒惰。

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

    但我仍然不确定标准的Haskell 2010是否会破坏引用透明性,或者惰性IO本身是不好的。 至少看起来完全有可能建立一个RealWorld的小型模型,在这个模型中,惰性IO是完全安全和可预测的。 并且可能有足够好的近似值,可以在不破坏参考透明度的情况下提供许多实用的目的。


    在顶部,你拥有的两个功能总是相同的。

    v1 = do !a <- x
            y
    
    v2 = do !a <- unsafeInterleaveIO x
            y
    

    请记住, unsafeInterleaveIO延迟IO操作,直到它的结果被强制为止 - 但是您会立即使用严格的模式匹配!a来强制执行它,所以操作不会被延迟。 所以v1v2是完全一样的。

    一般来说

    一般情况下,您需要证明您使用unsafeInterleaveIO是安全的。 如果您调用unsafeInterleaveIO x ,那么您必须证明可以随时调用x并仍然产生相同的输出。

    关于懒惰IO的现代情绪

    ......懒惰IO有99%的时间是危险的和坏主意。

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

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

    今天,人们正在通过使用Conduit或Pipes等库来以不同方式解决增量IO问题。 Conduit和Pipes比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将使其工作。

    链接

    来自惰性IO在Haskell邮件列表中由Oleg Kiselyov打破纯度:

    我们演示懒惰IO如何中断参考透明度。 Int->Int->Int类型的纯函数根据其参数的评估顺序给出不同的整数。 我们的Haskell98代码只使用标准输入。 我们得出结论:赞美Haskell的纯度和广告惰性IO是不一致的。

    ...

    惰性IO不应被认为是很好的风格。 纯度的一个常见定义是纯表达式应该评估为相同的结果,而不考虑评估顺序,或者等于可以代替等于。 如果Int类型的表达式评估为1,我们应该能够用1替换每个出现的表达式而不改变结果和其他可观察值。

    从Haskell邮件列表中的Oleg Kiselyov的惰性vs正确IO:

    毕竟,有什么比Haskell的精神更能反映出一种具有可观察副作用的“纯粹”功能。 使用惰性IO,确实必须在正确性和性能之间进行选择。 这样的代码的出现在Lazy IO的死锁证据出现之后特别奇怪,它在不到一个月的时间里出现在这个列表中。 更不用说不可预测的资源使用情况和依靠终结器来关闭文件(忘记GHC不能保证终结器将被运行)。

    Kiselyov写了Iteratee库,这是懒惰IO的第一个真正的替代品。


    懒惰意味着什么时候(和是否)确切地执行计算取决于何时(以及是否)运行时实现决定它需要该值。 作为一名Haskell程序员,您完全放弃了对评估顺序的控制权(除非您的代码中固有的数据依赖性,以及您开始严格执行以强制运行时进行某些选择)。

    对于纯粹的计算来说,这是很好的,因为每当你做一个纯粹的计算时,结果都是完全相同的(除了如果你进行计算,你实际上并不需要,你可能会遇到错误或无法终止,当另一个评估顺序可能允许程序成功终止;但任何评估顺序计算的所有非底部值都是相同的)。

    但是当你编写依赖IO的代码时,评估顺序很重要。 IO的全部意义在于提供一种构建计算的机制,这些计算的步骤取决于并影响程序之外的世界,并且这样做的一个重要部分是这些步骤被明确地排序。 使用unsafeInterleaveIO会抛弃明确的顺序,并放弃何时(以及是否) IO操作实际执行到运行时系统的控制。

    这对于IO操作来说通常是不安全的,因为它们的副作用之间可能存在依赖关系,这些依赖关系无法从程序内的数据依赖关系中推断出来。 例如,一个IO操作可能会创建一个包含一些数据的文件,而另一个IO操作可能会读取同一个文件。 如果它们都是“懒惰”执行的,那么只有在需要生成Haskell值时才会运行它们。 创建文件可能是IO () ,但很可能()是不需要的。 这可能意味着读取操作首先执行,要么是失败,要么是读取文件中已有的数据,而是读取其他操作应该放在那里的数据。 不能保证运行时系统将以正确的顺序执行它们。 要正确地编写一个总是为IO此操作的系统,您必须能够准确预测Haskell运行时将选择执行各种IO操作的顺序。

    unsafeInterlaveIO视为对编译器的承诺(它不能验证,它只会相信你), 执行 IO操作时是无关紧要的,还是完全消除了IO操作。 这实际上是所有unsafe*功能; 它们提供的设施一般不安全,并且安全性不能自动检查,但在特定情况下可能是安全的。 你有责任确保你使用它们实际上是安全的。 但是,如果你向编译器作出承诺,并且你的承诺是错误的,那么结果就是令人不快的错误。 名字中的“不安全”是吓唬你考虑你的特定情况,并决定你是否真的可以向编译器作出承诺。


    基本上,问题中的“更新”下的所有内容都很混乱,甚至没有错,所以当你试图理解我的答案时,请尽量忘掉它。

    看看这个功能:

    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个元素实际上是从文件中读取的第一行。

    链接地址: http://www.djcxy.com/p/1149.html

    上一篇: When is unsafeInterleaveIO unsafe?

    下一篇: What optimizations can GHC be expected to perform reliably?