8

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?

Anupam Jain
  • 7,851
  • 2
  • 39
  • 74
  • Here's an idea I just had - define `m>>=f` to have the following behaviour - Evaluate `f _|_`, and see if the result is not `_|_`. If so then that is the result. Else the result is `m>>=f`. However I'm not sure if that covers all the cases. And how do I compare with `_|_`? Try catch? – Anupam Jain Aug 25 '11 at 13:43
  • That's going to be hell on side effects. Which order are you going to work in for nesting? Depth-first or depth last? Either way, you're screwed. I don't think any order will work for your example and for `lift getLine >>= \s -> lift getLine >> putStr s` – rampion Aug 25 '11 at 13:49

3 Answers3

11

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.

Community
  • 1
  • 1
hammar
  • 138,522
  • 17
  • 304
  • 385
  • 5
    "Lazy IO" in the sense of things like `readFile` are sufficiently well-behaved in particular (somewhat common) scenarios, and aren't universally frowned upon if used properly. I'm [not really a fan of the concept](http://stackoverflow.com/questions/6668716/haskell-lazy-bytestring-read-write-progress-function/6669453#6669453), but opinions differ. Tossing `unsafeInterleaveIO` in arbitrary places, on the other hand, is very dubious indeed. – C. A. McCann Aug 25 '11 at 14:15
  • 11
    This monad is evil incarnate and will populate my nightmares. – John L Aug 25 '11 at 14:43
8

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.

C. A. McCann
  • 76,893
  • 19
  • 209
  • 302
  • I don't think the author's intent is malicious. The problem of lazy IO seems important to me (build scripts can be one example) and there should be some good ways to achieve it (not necessarily using something as dubious as `unsafeInterleaveIO`). – Rotsor Aug 25 '11 at 15:32
  • A separate question is how is `unsafeInterleaveIO` unsafe. I don't see how it's worse than `forkIO` for example. – Rotsor Aug 25 '11 at 15:36
  • @Rotsor: It's not a matter of malicious intent, just being misguided. The existing lazy IO functions work well in certain contexts, but extending the concept will result in programs whose behavior you can't hope to reason clearly about, and can't cope with adding certain kinds of features later. I'm sure the author is trying to solve a reasonable problem, but this is an unreasonable solution. – C. A. McCann Aug 25 '11 at 15:38
  • @Rotsor: Rather than repeating myself, look at [this older post of mine](http://stackoverflow.com/questions/6668716/haskell-lazy-bytestring-read-write-progress-function/6669453#6669453), which I mentioned in a comment on hammar's answer, to see a scenario where lazy IO fails and an example of an alternate, more explicit style that I prefer. – C. A. McCann Aug 25 '11 at 15:42
  • @Rotsor: As for comparisons to `forkIO`, using `unsafeInterleaveIO` is worse exactly because it's implicit and non-local. A new thread is a tangible thing running independently, whereas deferred `IO` actions happen implicitly at (possibly unclear) points *caused by* the evaluation of a pure computation. This introduces all the usual difficulty of reasoning about lazy evaluation, with the bonus of worrying about side effects as well. A weaker notion of purity is retained, but sanity dwindles rapidly... – C. A. McCann Aug 25 '11 at 15:48
5

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) }
MasterMastic
  • 20,711
  • 12
  • 68
  • 90
ertes
  • 2,287
  • 15
  • 10