19

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 to know the condition for both parallel/concurrent and the single threaded usage.

More specifically, are the two functions in the following code semantically equivalent? If not, when and how?


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'

Here's how I would use this in practice:


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

Although I would rather avoid using this code in serious applications because of the terrifying unsafe* stuff, I could at least be lazier than would be possible with the stricter IO monad in deciding what 'convergence' means, leading to (what I think is) more idiomatic Haskell. And this brings up another question:why is it not the default semantics for Haskell's (or GHC's?) IO monad? I've heard some resource management issues for lazy IO (which GHC only provides by a small fixed set of commands), but the examples typically given somewhat resemble like a broken makefile:a resource X depends on a resource Y, but if you fail to specify the dependency, you get an undefined status for X. Is lazy IO really the culprit for this problem? (On the other hand, if there is a subtle concurrency bug in the above code such as deadlocks I would take it as a more fundamental problem.)

Update

Reading Ben's and Dietrich's answer and his comments below, I have briefly browsed the ghc source code to see how the IO monad is implemented in GHC. Here I summerize my few findings.

  1. GHC implements Haskell as an impure, non-referentially-transparent language. GHC's runtime operates by successively evaluating impure functions with side effects just like any other functional languages. This is why the evaluation order matters.

  2. unsafeInterleaveIO is unsafe because it can introduce any kind of concurrency bugs even in a sigle-threaded program by exposing the (usually) hidden impurity of GHC's Haskell. (iteratee seems to be a nice and elegant solution for this, and I will certainly learn how to use it.)

  3. the IO monad must be strict because a safe, lazy IO monad would require a precise (lifted) representation of the RealWorld, which seems impossible.

  4. It's not just the IO monad and unsafe functions that are unsafe. The whole Haskell (as implemented by GHC) is potentially unsafe, and 'pure' functions in (GHC's) Haskell are only pure by convention and the people's goodwill. Types can never be a proof for purity.

To see this, I demonstrate how GHC's Haskell is not referentially transparent regardless of the IO monad, regardless of the unsafe* functions,etc.


-- 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

Just for easy reference, I collected some of the relevant definitions for the IO monad as implemented by GHC. (All the paths below are relative to the top directory of the ghc's source repository.)


--  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".

So, for example, ignoring the constructor and assuming referential transparency, we have


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

This is not what we would normally get from binding usual lazy state monads. Assuming the state variable s carries some real meaning (which it does not), it looks more like a concurrent IO (or interleaved IO as the function rightly says) than a lazy IO as we would normally mean by 'lazy state monad' wherein despite the laziness the states are properly threaded by an associative operation.

I tried to implement a truely lazy IO monad, but soon realized that in order to define a lazy monadic composition for the IO datatype, we need to be able to lift/unlift the RealWorld. However this seems impossible because there is no constructor for both State# s and RealWorld. And even if that were possible, I would then have to represent the precise, functional represenation of our RealWorld which is impossible,too.

But I'm still not sure whether the standard Haskell 2010 breaks referential transparency or the lazy IO is bad in itself. At least it seems entirely possible to build a small model of the RealWorld on which the lazy IO is perfectly safe and predictable. And there might be a good enough approximation that serves many practical purposes without breaking the referential transparency.

mnish
  • 1,869
  • 1
  • 13
  • 15

4 Answers4

18

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.

Cactus
  • 27,075
  • 9
  • 69
  • 149
Dietrich Epp
  • 205,541
  • 37
  • 345
  • 415
  • How so? `unsafeInterleaveIO x` has type `IO a` and should be able to produce different results (e.g. the current time of the evaluation). – mnish Nov 07 '12 at 05:48
  • "How so?" I'm sorry, but please be more specific, or use quotes. I don't know what part of what you're talking about. – Dietrich Epp Nov 07 '12 at 05:51
  • Sorry, I mean this part:"then you have to prove that x can be called at any time and still produce the same output." – mnish Nov 07 '12 at 05:55
  • When you call `getTime :: IO Time` or whatever, that's fine because it's in the IO monad and you get the current time. But when you use `unsafeInterleaveIO getTime :: IO Time` it *looks* like it does the same thing -- get the current time -- but it doesn't. It gets the time when the result is forced. This is surprising. – Dietrich Epp Nov 07 '12 at 06:01
  • Also, I could argue with your statement "Lazy IO's solution is to do IO outside of the IO monad". My naive view is that lazy IO doesn't do any IO outside of the IO monad. They just sometimes help the programmer to perform pure computation on *undefined values* in the sense I implied in the question (namely the failure to specify the precise dependency under which the computed value is well-defined.) What is the modern view on this? – mnish Nov 07 '12 at 06:12
  • Could you define the term "undefined value"? It has no technical meaning in Haskell that I am aware of... – Dietrich Epp Nov 07 '12 at 06:17
  • @mnish: I don't know where you got the idea that lazy IO doesn't do any IO outside of the IO monad. The entire purpose of `unsafeInterleaveIO` is to defer an IO computation so it is executed outside the IO monad -- in other words, doing IO outside of the IO monad is exactly what characterizes lazy IO. – Dietrich Epp Nov 07 '12 at 06:19
  • Could you wait a minute please? I remember I've recently read Oleg Kiselyov's argument and his examples about this somewhere.. – mnish Nov 07 '12 at 06:24
  • @mnish: I've posted some of Kiselyov's writings about lazy IO in the body of the answer. – Dietrich Epp Nov 07 '12 at 06:39
  • Ok I've read it throught a stackoverflow link! 'http://www.mail-archive.com/haskell@haskell.org/msg21782.html'. In the example given there, both `s1` and `s2` are what I mean `undefined values`. They are the results of `hGetContents` for some handles,but their value really depends on the order of read operation,the seek position at the moment of evaluation,etc. In a makefile I would write those dependencies as `s2: s1 h1 h2` or something like that. What I'm asking is, is the lazy-IO (or any evaluation order) the culprit for those undefined-ness of the values? – mnish Nov 07 '12 at 06:47
  • Isn't some kind of partial ordering of resource dependencies enough (as in make)? If so, evaluation order per se seems to be irrelevant. – mnish Nov 07 '12 at 06:51
  • @mnish: In Haskell, the result of a computation is not supposed to depend on evaluation order. Evaluation order is indeterminate, and it's okay. If you use lazy IO, then suddenly you've broken that -- your program depends on evaluation order. So `s1` and `s2` get the wrong results. It is the fault of Lazy IO for producing values which depend on evaluation order. – Dietrich Epp Nov 07 '12 at 07:01
  • Well, the error message from your examples speaks the problem itself:"openFile: resource busy (file is locked)". The value of `x` really depends on the closing of the file because there might be some nasty kernel driver that tries to append some gibberish to the file. – mnish Nov 07 '12 at 07:03
  • @mnish: That's not quite correct -- the value of `x` is actually not the problem. The problem is that the evaluation of `x` causes an IO action to occur -- closing the file -- which should not occur, since `x` is not in the `IO` monad. Since the file is closed at an indeterminate time, it ends up happening *after* the program tries to open the file for writing. – Dietrich Epp Nov 07 '12 at 07:06
  • Dietrich, what I'm arguing is this:isn't the dependency of resources independent of the evaluation order? If some set of augmented IO actions allow me to more precisely specify the conditions the IO actions give valid results (by types), then wouldn't lazy IO compute the perfectly valid, predictable values (again as in make)? But reading your comments, it looks like I need to understand how IO monad is implemented in ghc. – mnish Nov 07 '12 at 07:15
  • Thank you Dietrich, and I'm sorry I think I need a bit of time to understand and accept your answer. – mnish Nov 07 '12 at 07:18
  • @mnish: I'm not trying to argue that Lazy IO is never safe here. But this isn't simple like makefile dependencies. `s1` does not depend on `s2`, instead, their values both depend on IO state. It's possible to use lazy IO in a relatively safe manner but it is difficult, and the type system will not help you do it. Sometimes Lazy IO is good enough and works, but sometimes it will really surprise you so you might want to learn iteratees or conduit or one of the alternatives. Unlike lazy IO, iteratees and conduit are known to be safe. – Dietrich Epp Nov 07 '12 at 07:19
  • 5
    I use lazy IO in 90% of all my code, and it works just fine. It all depends on your use case. – augustss Nov 07 '12 at 08:32
  • 4
    My rule of thumb for lazy `IO` is just to imagine that every lazy `IO` function is prefixed with an `unsafe` and program accordingly. – Gabriella Gonzalez Nov 07 '12 at 20:55
  • @Dietrich: Thank you for your comments, I think I have a better understanding how Haskell code is run by GHC. But I wonder, how about other implementations? Is there a more 'purer' approach to IO than GHC? – mnish Nov 09 '12 at 08:21
11

Laziness means that when (and whether) exactly a computation is actually carried out depends on when (and whether) the runtime implementation decides it needs the value. As a Haskell programmer you completely relinquish control over the evaluation order (except by the data dependencies inherent in your code, and when you start playing with strictness to force the runtime to make certain choices).

That's great for pure computations, because the result of a pure computation will be exactly the same whenever you do it (except that if you carry out computations that you don't actually need, you might encounter errors or fail to terminate, when another evaluation order might allow the program to terminate successfully; but all non-bottom values computed by any evaluation order will be the same).

But when you're writing IO-dependent code, evaluation order matters. The whole point of IO is to provide a mechanism for building computations whose steps depend on and affect the world outside the program, and an important part of doing that is that those steps are explicitly sequenced. Using unsafeInterleaveIO throws away that explicit sequencing, and relinquishes control of when (and whether) the IO operation is actually carried out to the runtime system.

This is unsafe in general for IO operations, because there may be dependencies between their side-effects which cannot be inferred from the data dependencies inside the program. For example, one IO action might create a file with some data in it, and another IO action might read the same file. If they're both executed "lazily", then they'll only get run when the resulting Haskell value is needed. Creating the file is probably IO () though, and it's quite possible that the () is never needed. That could mean that the read operation is carried out first, either failing or reading data that was already in the file, but not the data that should have been put there by the other operation. There's no guarantee that the runtime system will execute them in the right order. To program correctly with a system that always did this for IO you'd have to be able to accurately predict the order in which the Haskell runtime will choose to perform the various IO actions.

Treat unsafeInterlaveIO as promise to the compiler (which it cannot verify, it's just going to trust you) that it doesn't matter when the IO action is carried out, or whether it's elided entirely. This is really what all the unsafe* functions are; they provide facilities that are not safe in general, and for which safety cannot be automatically checked, but which can be safe in particular instances. The onus is on you to ensure that your use of them is in fact safe. But if you make a promise to the compiler, and your promise is false, then unpleasant bugs can be the result. The "unsafe" in the name is to scare you into thinking about your particular case and deciding whether you really can make the promise to the compiler.

Ben
  • 68,572
  • 20
  • 126
  • 174
  • I can now confirm that you are perfectly right. The point is that unlike the usual state monad where there is a functional relations between states, the 'state' in the IO monad (which is represented as `State# RealWorld`) is actually threaded by _evaluation_, not by any functional relation. – mnish Nov 09 '12 at 08:30
  • 1
    `unsafeInterleaveIO (a >>= b) >>= c` does preserve the explicit sequencing between a and b but not between b and c. – Jeremy List Jan 19 '15 at 09:29
4

Basically everything under "Update" in the question is so confused it's not even wrong, so please try to forget it when you're trying to understand my answer.

Look at this function:

badLazyReadlines :: Handle -> IO [String]
badLazyReadlines h = do
  l <- unsafeInterleaveIO $ hGetLine h
  r <- unsafeInterleaveIO $ badLazyReadlines h
  return (l:r)

In addition to what I'm trying to illustrate: the above function also doesn't handle reaching the end of the file. But ignore that for now.

main = do
  h <- openFile "example.txt" ReadMode
  lns <- badLazyReadlines h
  putStrLn $ lns ! 4

This will print the first line of "example.txt", because the 5th element in the list is actually the first line that's read from the file.

Jeremy List
  • 1,756
  • 9
  • 16
  • I assume the solution to this would be to remove both `unsafeInterleaveIO` and move them to the beginning of the function, so we get `goodLazyReadLines h = unsafeInterleaveIO $ do { l <- hGetLine h; r <- goodLazyReadLines h; return (l:r)}`? This way traversing the list will also force each element so far in the list. – Hjulle Dec 06 '19 at 14:46
2

Your joinIO and joinIO' are not semantically equivalent. They will usually be the same, but there's a subtlety involved: a bang pattern makes a value strict, but that's all it does. Bang patterns are implemented using seq, and that does not enforce a particular evaluation order, in particular the following two are semantically equivalent:

a `seq` b `seq` c
b `seq` a `seq` c

GHC can evaluate either b or a first before returning c. Indeed, it can evaluate c first, then a and b, then return c. Or, if it can statically prove a or b are non-bottom, or that c is bottom, it doesn't have to evaluate a or b at all. Some optimisations do genuinely make use of this fact, but it doesn't come up very often in practice.

unsafeInterleaveIO, by contrast, is sensitive to all or any of those changes – it does not depend on the semantic property of how strict some function is, but the operational property of when something is evaluated. So all of the above transformations are visible to it, which is why it's only reasonable to view unsafeInterleaveIO as performing its IO non-deterministically, more or less whenever it feels appropriate.

This is, in essence, why unsafeInterleaveIO is unsafe - it is the only mechanism in normal use that can detect transformations that ought to be meaning-preserving. It's the only way you can detect evaluation, which by rights ought to be impossible.

As an aside, it's probably fair to mentally prepend unsafe to every function from GHC.Prim, and probably several other GHC. modules as well. They're certainly not ordinary Haskell.

Ben Millwood
  • 6,754
  • 24
  • 45
  • I know I'm still a noob and naive, but I can't resist to say this: those 'semantically' things only make sense for a pure, referentially transparent language in which the Church-Rosser holds, which (GHC's) Haskell is not. `unsafe*` are unsafe only because they leak this little secret, not by their inherent nature. Why don't they implement the `IO` as a primitive, and primitive functions as `IO a`? – mnish Nov 10 '12 at 05:29
  • 1
    @mnish: it's sort of a matter of perspective what you think of as part of Haskell and what isn't. I think it's reasonable to say that the GHC-internal modules are implementation details and not part of the language – in some sense they are non-Haskell code, but written with Haskell syntax and compiled with a Haskell compiler. The semantics are perfectly okay regardless of little secrets: if you declare any secret-leaking as outside the language, then inside the language, the semantics hold. – Ben Millwood Nov 12 '12 at 22:40
  • But @Ben, the problems is that the unsafety can be viral --- the correctness of the whole calculus can be broken by some innocent-looking operations down below. Who knows that the standard 'lazy IO' functions never break the operational semantics of GHC? – mnish Nov 13 '12 at 06:22
  • Well... if they do, it's a bug, just like it would be a bug if any other aspect of GHC's implementation of the semantics was incorrect. I don't see what you mean by implementing the `IO` as a primitive - it *is* primitive, but it's just a primitive written in the "pseudo-Haskell" that all the GHC.* modules are written. (and which functions are "primitive functions?") – Ben Millwood Nov 14 '12 at 12:54
  • 2
    I know I'm a couple years late, but bang patterns are *not* implemented using `seq`; in fact, the reverse is true. Bang patterns are translated to `case` forms in GHC Core, and `case` in Core is always strict. For instance, `let !x = foo in bar` will be translated directly to something like `case foo of x {DEFAULT -> bar}`. – dfeuer Jan 19 '15 at 15:42
  • @dfeuer: acknowledged, thanks. I'm not particularly fond of this answer so I'm not going to bother fixing it, but you are welcome to if you like. – Ben Millwood Jan 20 '15 at 13:29