When is unsafeInterleaveIO unsafe?

前端 未结 4 759
失恋的感觉
失恋的感觉 2021-01-31 05:43

Unlike other unsafe* operations, the documentation for unsafeInterleaveIO is not very clear about its possible pitfalls. So exactly when is it unsafe? I would like

4条回答
  •  余生分开走
    2021-01-31 05:55

    At the top, the two functions you have are always identical.

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

    Remember that unsafeInterleaveIO defers the IO operation until its result is forced -- yet you are forcing it immediately by using a strict pattern match !a, so the operation is not deferred at all. So v1 and v2 are exactly the same.

    In general

    In general, it is up to you to prove that your use of unsafeInterleaveIO is safe. If you call unsafeInterleaveIO x, then you have to prove that x can be called at any time and still produce the same output.

    Modern sentiment about Lazy IO

    ...is that Lazy IO is dangerous and a bad idea 99% of the time.

    The chief problem that it is trying to solve is that IO has to be done in the IO monad, but you want to be able to do incremental IO and you don't want to rewrite all of your pure functions to call IO callbacks to get more data. Incremental IO is important because it uses less memory, allowing you to operate on data sets that don't fit in memory without changing your algorithms too much.

    Lazy IO's solution is to do IO outside of the IO monad. This is not generally safe.

    Today, people are solving the problem of incremental IO in different ways by using libraries like Conduit or Pipes. Conduit and Pipes are much more deterministic and well-behaved than Lazy IO, solve the same problems, and do not require unsafe constructs.

    Remember that unsafeInterleaveIO is really just unsafePerformIO with a different type.

    Example

    Here is an example of a program that is broken due to lazy 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"
    

    This program will not work. Replacing the lazy IO with strict IO will make it work.

    Links

    From Lazy IO breaks purity by Oleg Kiselyov on the Haskell mailing list:

    We demonstrate how lazy IO breaks referential transparency. A pure function of the type Int->Int->Int gives different integers depending on the order of evaluation of its arguments. Our Haskell98 code uses nothing but the standard input. We conclude that extolling the purity of Haskell and advertising lazy IO is inconsistent.

    ...

    Lazy IO should not be considered good style. One of the common definitions of purity is that pure expressions should evaluate to the same results regardless of evaluation order, or that equals can be substituted for equals. If an expression of the type Int evaluates to 1, we should be able to replace every occurrence of the expression with 1 without changing the results and other observables.

    From Lazy vs correct IO by Oleg Kiselyov on the Haskell mailing list:

    After all, what could be more against the spirit of Haskell than a `pure' function with observable side effects. With Lazy IO, one indeed has to choose between correctness and performance. The appearance of such code is especially strange after the evidence of deadlocks with Lazy IO, presented on this list less than a month ago. Let alone unpredictable resource usage and reliance on finalizers to close files (forgetting that GHC does not guarantee that finalizers will be run at all).

    Kiselyov wrote the Iteratee library, which was the first real alternative to lazy IO.

提交回复
热议问题