Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Truly" lazy IO in Haskell

Tags:

haskell

monads

Consider the fragment -

getLine >>= \_ -> getLine >>= putStr

It does the reasonable thing, asking for a string twice, and then printing the last input. Because the compiler has no way of knowing what outside effects getLine has, it has to execute both of them, even though we throw away the result of the first one.

What I need is to wrap the IO Monad into another Monad (M) that allows IO computations to be effectively NOPs unless their return values are used. So that the program above could be rewritten as something like -

runM $ lift getLine >>= \_ -> lift getLine >>= lift putStr

Where

runM :: M a -> IO a
lift :: IO a -> M a

And the user is asked for input only once.

However, I cannot figure out how to write this Monad to achieve the effect I want. I'm not sure if it's even possible. Could someone please help?

like image 229
Anupam Jain Avatar asked Aug 25 '11 13:08

Anupam Jain


3 Answers

Lazy IO is usually implemented using unsafeInterleaveIO :: IO a -> IO a, which delays the side effects of an IO action until its result is demanded, so we'll probably have to use that, but let's get some minor problems out of the way first.

First of all, lift putStr would not type check, as putStr has type String -> IO (), and lift has type IO a -> M a. We'll have to use something like lift . putStr instead.

Secondly, we're going to have to differentiate between IO actions that should be lazy and those who should not. Otherwise the putStr will never be executed, as we're not using it's return value () anywhere.

Taking that into account, this seems to work for your simple example, at least.

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

import System.IO.Unsafe

newtype M a = M { runM :: IO a }
    deriving (Monad)

lazy :: IO a -> M a
lazy = M . unsafeInterleaveIO

lift :: IO a -> M a
lift = M

main = runM $ lazy getLine >> lazy getLine >>= lift . putStr

However, as C. A. McCann points out, you should probably not use this for anything serious. Lazy IO is frowned upon already, as it makes it difficult to reason about the actual order of the side effects. This would make it even harder.

Consider this example

main = runM $ do
    foo <- lazy readLn
    bar <- lazy readLn
    return $ foo / bar

The order of the two numbers are read in will be completely undefined, and may change depending on compiler version, optimizations or the alignment of the stars. The name unsafeInterleaveIO is long and ugly for a good reason: to remind you of the dangers of using it. It's a good idea to let people know when it's being used and not hide it in a monad.

like image 153
hammar Avatar answered Oct 16 '22 08:10

hammar


There's no sensible way to do this, because to be quite honest it's not really a sensible thing to do. The entire purpose for introducing monadic I/O was to give a well-defined ordering to effects in the presence of lazy evaluation. It is certainly possible to throw that out the window if you really must, but I'm not sure what actual problem this would solve other than making it easier to write confusingly buggy code.

That said, introducing this sort of thing in a controlled fashion is what "Lazy IO" already does. The "primitive" operation for that is unsafeInterleaveIO, which is implemented roughly as return . unsafePerformIO, plus some details to make things behave a bit nicer. Applying unsafeInterleaveIO to everything, by hiding it in the bind operation of your "lazy IO" monad, would probably accomplish the ill-advised notion you're after.

like image 39
C. A. McCann Avatar answered Oct 16 '22 08:10

C. A. McCann


What you are looking for isn't really a monad, unless you want to work with unsafe stuff like unsafeInterleaveIO.

Instead, a much cleaner abstraction here is Arrow.
I think, the following could work:

data Promise m a
    = Done a
    | Thunk (m a)

newtype Lazy m a b =
    Lazy { getLazy :: Promise m a -> m (Promise m b) }
like image 5
ertes Avatar answered Oct 16 '22 08:10

ertes