[Haskell-cafe] WriterT [w] IO is not lazy in reading [w]

Ryan Ingram ryani.spam at gmail.com
Thu Jan 1 05:17:59 EST 2009


2008/12/31 Paolino <paolo.veronelli at gmail.com>:
> I must ask why runWriterT k :: State s (a,[Int]) is working.
> Looks like I could runIO the same way I evalState there.
> In that case I wouldn't wait for the State s action to finish.
>
> Thanks

Assuming you have Control.Monad.State.Lazy (which I think is the
default), here is the difference:

(>>=) for State:

ma >>= f = State $ \s0 ->
    let (a, s1) = runState ma s0
    in runState (f a) s1

The "let" is a lazy pattern match; in particular, consider this code:

    runState (do
        put 0
        put 1) 2

-> desugar
    runState (put 0 >> put 1) 2

The rest is lazy evaluation in action!
-> apply >>
    runState (put 0 >>= \_ -> put 1) 2

-> apply >>=
    runState (State $ \s0 ->
        let (a, s1) = runState (put 0) s0
        in runState (put 1) s1
    ) 2

-> apply runState
    (\s0 ->
        let (a,s1) = runState (put 0) s0
        in runState (put 1) s1
   ) 2

-> beta reduce
    let s0 = 2 in
    let (a,s1) = runState (put 0) s0
    in runState (put 1) s1

-> apply put
    let s0 = 2 in
    let (a,s1) = runState (put 0) s0
    in runState (State $ \_ -> ((), 1)) s1

-> apply runState
    let s0 = 2 in
    let (a,s1) = runState (put 0) s0
    in (\_ -> ((), 1)) s1

-> beta reduce
    let s0 = 2 in
    let (a,s1) = runState (put 0) s0
    in ((), 1)

-> garbage collect
    ((), 1)

Note that at no point were s0, a, or s1 evaluated; so there was no
need to evaluate the state at all!  Furthermore, even if you replace
(put 0) with a computation that loops and looks at the state at each
point, the result is immediately a pair, so future computation can
continue.  Now lets contrast with IO:

(>>=) for IO (taking out the "unboxed pairs" stuff which muddles the issue)

ma >>= f = IO $ \s0 ->
    case (unIO ma s0) of
        (a, s1) -> unIO (f a) s1

(Note that this is the basically the same as Control.Monad.State.Strict)

Consider

main = do
    putStrLn "hello"
    putStrLn "world"

We'll use this definition of putStrLn for simplicity's sake:

> -- primitive, unsafe, side-effecting function.
> putStrLn# :: String -> ()
>
> putStrLn :: String -> IO ()
> putStrLn s = IO $ \w -> seq (putStrLn# s) ((), w)

So putStrLn is an IO action which takes a "world" (a dummy value that
doesn't really exist) as input, and before it gives any output, forces
the evaluation of a side-effecting primitive function via seq.  It
then returns a pair containing the result (), and the original "world"
argument as the state.

Desugar:
    main = (putStrLn "hello" >> putStrLn "world")

Send to the runtime:
    unIO (putStrLn "hello" >> putStrLn "world") world#

Now lets evaluate it.

-> Apply >>
    unIO (putStrLn "hello" >>= \_ -> putStrLn "world") world#

-> Apply >>=
    unIO (IO $ \w0 -> case unIO (putStrLn "hello") w0 of
        (a, w1) -> unIO ((\_ -> putStrLn "world") a) w1

-> Apply unIO and beta-reduce
    case unIO (putStrLn "hello") world# of
        (a, w1) -> unIO ((\_ -> putStrLn "world") a) w1

Note that the evaluation order is different here; since we have a case
statement, we must force the evaluation of the first putStrLn to make
sure it evaluates to a pair!

-> Apply putStrLn
    case unIO (IO $ \w -> seq (putStrLn# "hello") ((), w)) world# of
        (a, w1) -> unIO ((\_ -> putStrLn "world") a) w1

-> apply unIO & beta-reduce
    case seq (putStrLn# "hello") ((), world#) of
        (a, w1) -> unIO ((\_ -> putStrLn "world") a) w1

-> seq evaluates putStrLn#, side effect happens here!
    case ((), world#) of
        (a, w1) -> unIO ((\_ -> putStrLn "world") a) w1

-> pattern match in case succeeds now
    let a = ()
        w1 = world#
    in unIO ((\_ -> putStrLn "world") a) w1

-> beta reduce & garbage collect
    unIO (putStrLn "world") world#

-> apply putStrLn
    unIO (IO $ \w -> seq (putStrLn# "world") ((), w)) world#

-> apply unIO & beta reduce
    seq (putStrLn# "world") ((), world#)

-> seq evaluates putStrLn#, side effect happens here!
    ((), world#)

Now,  you are probably wondering for the reason behind the use of
"case" vs. "let".  It's pretty simple; lets evaluate that last code
using the lazy method like State.  Everything is the same up to "apply
>>=", so start there:

    unIO (putStrLn "hello" >>= \_ -> putStrLn "world") world#

-> Apply >>=
    unIO (IO $ \w0 ->
        let (a, w1) = unIO (putStrLn "hello") w0
        in unIO (putStrLn "world") w1
    ) world#

-> apply unIO and beta-reduce
    let (a, w1) = unIO (putStrLn "hello") world#
    in unIO (putStrLn "world") w1

-> apply putStrLn (the second one!)
    let (a, w1) = unIO (putStrLn "hello") world#
    in unIO (IO $ \w -> seq (putStrLn# "world") ((), w)) w1

-> apply unIO and beta-reduce
    let (a, w1) = unIO (putStrLn "hello") world#
    in seq (putStrLn# "world" w1) ((), w1)

Now we call putStrLn# which causes a side effect.  It ignores the
"world" argument and just returns it unchanged, of course.  We could
also add extra machinery to the compiler to force it to treat the
"world" argument as strictly, but you can see that using "lazy" really
changes the order of evaluation, which ends up making the code much
harder to optimize.

-> seq evaluates putStrLn# (printing "world", oops!)
    let (a,w1) = unIO (putStrLn "hello" world#)
    in ((), w1)

We need w1 here, so we have to force evaluation of the pair to extract it.

-> apply putStrLn
    let (a,w1) = unIO (IO $ \w -> seq (putStrLn# "hello") ((), w)) world#
    in ((), w1)

-> apply unIO & beta-reduce
    let (a,w1) = seq (putStrLn# "hello") ((), world#)
    in ((), w1)

-> seq evaluates putStrLn#, printing "hello"
    let (a,w1) = ((), world#)
    in ((), w1)

-> garbage-collect
    ((), world#)

I've omitted some of the details, and this isn't exactly how it works.
 In particular, I believe GHC's primitive operations take the World#
argument directly, so you can argue that this behavior wouldn't happen
as long as that argument was considered "strict".  But the "lazy"
behavior makes the order of evaluation much harder to predict, which I
think is considered to be bad for IO, which lives on the "imperative"
side of the imperative/functional divide.  In particular,
unsafeInterleaveIO (and its friend getContents) would be even more
difficult to use correctly if IO used a "lazy state" monad.

It might be possible to build a "lazy-ify" monad transformer which
would make this work.  In particular, I think Oleg's LogicT
transformer[1] does something of the sort, only applying side effects
where they are required in order to get "the next result" in a
nondeterministic computation.

  -- ryan

[1] http://okmij.org/ftp/Computation/monads.html#LogicT


More information about the Haskell-Cafe mailing list