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

时间:2012-11-07 05:35:57

标签: haskell ghc lazy-evaluation

与其他不安全的*操作不同,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。在这里,我总结了我的一些发现。

  1. GHC将Haskell实现为一种不纯的,非引用透明的语言。 GHC的运行时通过连续评估具有副作用的不纯函数来运行,就像任何其他函数语言一样。这就是评估订单重要的原因。

  2. unsafeInterleaveIO是不安全的,因为它可以通过暴露GHC的Haskell的(通常)隐藏的杂质来引入任何类型的并发错误,即使在sigle-threaded程序中也是如此。 (iteratee似乎是一个很好的优雅解决方案,我一定会学习如何使用它。)

  3. IO monad必须严格,因为安全,懒惰的IO monad需要RealWorld的精确(提升)表示,这似乎是不可能的。

  4. 不仅IO monad和unsafe函数不安全。整个Haskell(由GHC实现)可能是不安全的,(GHC)Haskell中的“纯粹”功能只是按照惯例和人们的善意纯粹。类型永远不能证明纯度。

  5. 为了看到这一点,我演示了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# sRealWorld都没有构造函数。即使这是可能的,我也必须代表RealWorld的精确,功能性代表,这也是不可能的。

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

4 个答案:

答案 0 :(得分:17)

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

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非常重要,因为它占用的内存较少,允许您在不改变算法的情况下对不适合内存的数据集进行操作。

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

今天,人们通过使用ConduitPipes等库来以不同方式解决增量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)

您的joinIOjoinIO' 在语义上等效。它们通常是相同的,但有一个微妙的参与:一个爆炸模式使一个值严格,但这就是它的全部。 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行实际上是从文件中读取的第一行。