zoran119
zoran119

Reputation: 11317

Lazy evaluation of IO actions

I'm trying to write code in source -> transform -> sink style, for example:

let (|>) = flip ($)
repeat 1 |> take 5 |> sum |> print

But would like to do that using IO. I have this impression that my source can be an infinite list of IO actions, and each one gets evaluated once it is needed downstream. Something like this:

-- prints the number of lines entered before "quit" is entered
[getLine..] >>= takeWhile (/= "quit") >>= length >>= print

I think this is possible with the streaming libraries, but can it be done along the lines of what I'm proposing?

Upvotes: 2

Views: 440

Answers (3)

Clinton
Clinton

Reputation: 23135

The issue here is that Monad is not the right abstraction for this, and attempting to do something like this results in a situation where referential transparency is broken.

Firstly, we can do a lazy IO read like so:

module Main where

import System.IO.Unsafe (unsafePerformIO)
import Control.Monad(forM_)

lazyIOSequence :: [IO a] -> IO [a]
lazyIOSequence = pure . go where
    go :: [IO a] -> [a]
    go (l:ls) = (unsafePerformIO l):(go ls)


main :: IO ()
main = do
    l <- lazyIOSequence (repeat getLine)
    forM_ l putStrLn 

This when run will perform cat. It will read lines and output them. Everything works fine.

But consider changing the main function to this:

main :: IO ()
main = do
    l <- lazyIOSequence (map (putStrLn . show) [1..])
    putStrLn "Hello World"

This outputs Hello World only, as we didn't need to evaluate any of l. But now consider replacing the last line like the following:

main :: IO ()
main = do
    x <- lazyIOSequence (map (putStrLn . show) [1..])
    seq (head x) putStrLn "Hello World"

Same program, but the output is now:

1
Hello World

This is bad, we've changed the results of a program just by evaluating a value. This is not supposed to happen in Haskell, when you evaluate something it should just evaluate it, not change the outside world.

So if you restrict your IO actions to something like reading from a file nothing else is reading from, then you might be able to sensibly lazily evaluate things, because when you read from it in relation to all the other IO actions your program is taking doesn't matter. But you don't want to allow this for IO in general, because skipping actions or performing them in a different order can matter (and above, certainly does). Even in the reading a file lazily case, if something else in your program writes to the file, then whether you evaluate that list before or after the write action will affect the output of your program, which again, breaks referential transparency (because evaluation order shouldn't matter).

So for a restricted subset of IO actions, you can sensibly define Functor, Applicative and Monad on a stream type to work in a lazy way, but doing so in the IO Monad in general is a minefield and often just plain incorrect. Instead you want a specialised streaming type, and indeed Conduit defines Functor, Applicative and Monad on a lot of it's types so you can still use all your favourite functions.

Upvotes: 1

danidiaz
danidiaz

Reputation: 27766

Using the repeatM, takeWhile and length_ functions from the streaming library:

import Streaming
import qualified Streaming.Prelude as S

count :: IO ()
count = do r <- S.length_ . S.takeWhile (/= "quit") . S.repeatM $ getLine
           print r

Upvotes: 1

This seems to be in that spirit:

let (|>) = flip ($)
let (.>) = flip (.)

getContents >>= lines .> takeWhile (/= "quit") .> length .> print

Upvotes: 0

Related Questions