
Thomas Hallgren
Simon Marlow wrote:
The real problem is that lazy I/O injects side effects into the pure world of expressions. Haskell has a perfectly good system for encapsulating side effects - the IO monad. So why put these sneaky side effects into pure values?
Although monadic IO is nice in many ways, I think of its introduction in Haskell as a stopgap measure that solved the immediate problem of interfacing Haskell to traditional operating system's imperative APIs, but at the same time seems to have put a stop to further research into the proper integration of laziness and IO.
I have to admit a great deal of sympathy for this view. In Thomas's example we use a file to buffer intermediate results. I'd also note that lazy I/O makes sense when I/O operations are *independent*. For example, consider the following strawman function:
skipIt :: [String] -> [String] -> [Int] skipIt [] rs = map read rs skipIt (l:ls) rs = n : skipIt (drop n rs) ls where n = read l
Here we consume the two inputs at different, but interrelated, rates. If those inputs come from files, we'd like to read each file at exactly the required rate. Can we do this in the I/O monad? In this case, we probably can. When skipIt is a page or more of tightly-written code, it gets much harder---and we will inevitably find ourselves creating the "IO" and "non-IO" versions of our functions. The expression/statement dichotomy strikes again. Introducing eager evaluation makes the argument for lazy I/O *more* compelling. For example, we might eagerly evaluate a function which consumes input from a file as long as there is input available. No more input? Stop evaluating eagerly and be lazy. Indeed, I speculate [heh, heh] that we might be able to do away with threads for concurrency in many cases if we choose an approach like this one. After all, what is lazy evaluation but a mechanism for performing very tightly-coupled concurrency? Hybrid evaluation is simply a way to loosen that coupling a bit. Lots of fertile ground for future research here. Indeed, replacing threads with laziness could be a very compelling argument for using laziness---even if you're one of those people who doesn't give a hang about semantic purity. Yes, we'll need to think carefully about exceptions---but a good deal of thought has already gone into asynchronous exceptions and the Dynamic type, and having lazy I/O generate asynchronous exceptions of type IOError may work out quite nicely. Yes, we'll need to think more carefully before providing functions like hGetContents---the real mistake may be allowing use of the handle after the call. And yes, as always we'll need to be careful about space---we hope eagerness will help. By the way, Simon mentions that lazy I/O is complicated to implement in the presence of eager evaluation. I'll note that the real complexity of eagerness comes from the expression langauge. Once you've figured out how to capture the semantics of infinite computations, calls to error, and so forth, you've got a lot of mechanism at your disposal. Making that mechanism available to implement lazy I/O is actually pretty easy (or at least, I found it pretty easy in Eager Haskell). There are other applications for this machinery we've built. We ought to seek them out. -Jan-Willem Maessen jmaessen@alum.mit.edu