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!
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.
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.
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\"]"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With