Friday, April 29, 2011

Haskell streams with IO effects

Consider the following Haskell program. I am trying to program in a "stream style" where functions operate on streams (implemented here simply as lists). Things like normalStreamFunc work great with lazy lists. I can pass an infinite list to normalStreamFunc and effectively get out another infinite list, but with a function mapped onto each value. Things like effectfulStreamFunc do not work so well. The IO action means that I need to evaluate the entire list before I can pull off individual values. For example, the output of the program is this:

a
b
c
d
"[\"a\",\"b\"]"

but what I want is a way to write effectfulStreamFunc so that the program produces this:

a
b
"[\"a\",\"b\"]"

leaving the remaining actions unevaluated. I can imagine a solution using unsafePerformIO, but let's say I am taking that off the table. Here is the program:

import IO

normalStreamFunc :: [String] -> [String]
normalStreamFunc (x:xs) = reverse(x) : normalStreamFunc xs

effectfulStreamFunc :: [String] -> IO [String]
effectfulStreamFunc [] = return []
effectfulStreamFunc (x:xs) = do
    putStrLn x
    rest <- effectfulStreamFunc xs
    return (reverse(x):rest)

main :: IO ()
main = do
     let fns = ["a", "b", "c", "d"]
     es <- effectfulStreamFunc fns
     print $ show $ take 2 es

Update:

Thank you all for the helpful and thoughtful feedback. I had not seen the sequence operator before, that is helpful to know about. I had thought of a (less elegant) way to pass around IO (String) values instead of Strings, but for the style of programming that is of limited usefulness, since I want to other stream functions to act on the strings themselves, not on actions that can produce a string. But, based on thinking through the other responses, I think I see why this is unsolvable in general. In the simple case I presented, what I really wanted was the sequence operator, since I was thinking that the stream ordering implied an ordering on the actions. In fact, no such ordering is necessarily implied. This becomes clearer to me when I think about a stream function that takes two streams as input (e.g. pairwise addition two streams). If both "incoming" streams performed IO, the ordering of those IO actions is undefined (unless, of course, we define it by sequencing it ourselves in the IO monad). Problem solved, thank you all!

From stackoverflow
  • I do not really understand your main goal but your usage of putStrLn results in the evaluation of the entire list because it will evaluate the argument when performed. Consider

    import IO
    
    normalStreamFunc :: [String] -> [String]
    normalStreamFunc (x:xs) = reverse(x) : normalStreamFunc xs
    
    effectfulStreamFunc :: [String] -> IO [String]
    effectfulStreamFunc [] = return []
    effectfulStreamFunc (x:xs) = do
        rest <- effectfulStreamFunc xs
        return (reverse(x):rest)
    
    main :: IO ()
    main = do
         let fns = ["a", "b", undefined,"c", "d"]
         es <- effectfulStreamFunc fns
         print $ show $ take 2 es
    

    this results in "[\"a\",\"b\"]", while using the putStrLn version it results in an exception.

  • How about this code:

    import IO
    
    normalStreamFunc :: [String] -> [String]
    normalStreamFunc (x:xs) = reverse(x) : normalStreamFunc xs
    
    effectfulStreamFunc :: [String] -> [IO (String)]
    effectfulStreamFunc [] = []
    effectfulStreamFunc (x:xs) =
        let rest = effectfulStreamFunc xs in
            (putStrLn x >> return x) : rest
    
    main :: IO ()
    main = do
         let fns = ["a", "b", "c", "d"]
         let appliedFns = effectfulStreamFunc fns
         pieces <- sequence $ take 2 appliedFns
         print $ show $ pieces
    

    Rather than effectfulStreamFunc actually doing any IO, this one instead creates a list of IO actions to perform. (Note the type signature change.) The main function then takes 2 of those actions, runs them and prints the results:

    a
    b
    "[\"a\",\"b\"]"
    

    This works because the type IO (String) is just a function/value like any other which you can put into a list, pass around, etc. Note that the do syntax doesn't occur in "effectfulStreamFunc" - it is actually a pure function, despite the "IO" in its signature. Only when we run sequence on those in main do the effects actually occur.

    Nathan Sanders : If you still like the do notation, you could write the second clause of effectfulStreamFunc as: effectfulStreamFunc (x:xs) = let putAction = do putStrLn x return x in putAction : effectfulStreamFunc xs I think it reads a little better.
    Jesse Rusak : Good point. I guess the do syntax is orthogonal to the issue of actually running the monad.
  • As mentioned by Tomh, you can't really do this "safely", because you're breaking the referential transparency in Haskell. You're trying to perform side effects lazily, but the thing about laziness is that you aren't guaranteed in what order or whether things get evaluated, so in Haskell, when you tell it to perform a side effect, it is always performed, and in the exact order specified. (i.e. in this case the side effects from the recursive call of effectfulStreamFunc before the return, because that was the order they were listed) You can't do this lazily without using unsafe.

    You can try using something like unsafeInterleaveIO, which is how lazy IO (e.g. hGetContents) is implemented in Haskell, but it has its own problems; and you said that you don't want to use "unsafe" stuff.

    import System.IO.Unsafe (unsafeInterleaveIO)
    
    effectfulStreamFunc :: [String] -> IO [String]
    effectfulStreamFunc [] = return []
    effectfulStreamFunc (x:xs) = unsafeInterleaveIO $ do
        putStrLn x
        rest <- effectfulStreamFunc xs
        return (reverse x : rest)
    
    main :: IO ()
    main = do
         let fns = ["a", "b", "c", "d"]
         es <- effectfulStreamFunc fns
         print $ show $ take 2 es
    

    In this case the output looks like this

    "a
    [\"a\"b
    ,\"b\"]"
    

0 comments:

Post a Comment

Note: Only a member of this blog may post a comment.