2

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?

zoran119
  • 10,657
  • 12
  • 46
  • 88
  • 3
    In general, yes. This is exactly the sort of thing the `pipes` and `conduit` libraries (and several others too) exist to do. I don't have experience with either myself, so examples will have to come from others. – Carl Oct 05 '19 at 04:04

3 Answers3

1

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
danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • Related: https://stackoverflow.com/questions/47133634/the-haskell-way-to-do-io-loops-without-explicit-recursion/47143862#47143862 https://stackoverflow.com/questions/40317431/haskell-replace-mapm-in-a-monad-transformer-stack-to-achieve-lazy-evaluation-n/40317623#40317623 https://stackoverflow.com/questions/50178815/can-io-actions-be-sequenced-while-keeping-the-logic-in-a-pure-function/50181766#50181766 – danidiaz Oct 05 '19 at 09:33
  • Sure, that will work, but my question is why do I need a streaming library? Why isn't the "normal" Haskell lazyness enough here? – zoran119 Oct 05 '19 at 23:13
1

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.

Clinton
  • 22,361
  • 15
  • 67
  • 163
0

This seems to be in that spirit:

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

getContents >>= lines .> takeWhile (/= "quit") .> length .> print
  • Yes, but I see `getContents` as one action that's lazy. I'm specifically interested in multiple actions. – zoran119 Oct 05 '19 at 03:07