RE: "interact" behaves oddly if used interactively

Yes, the presence of lazy IO makes optimistic evaluation more complicated, but I do not see that it compromises the purity of the language in anyway (whatever purity is ;-).
So, we're agreed that the presence of lazy evaluation makes implementing optimistic evaluation (and other evaluation strategies) more complicated. Personally, I find it disturbing that the presence of this family of library functions affects something as low-level as the evaluation strategy of the language. Or, to put it another way, it enforces a particular evaluation strategy on a part of the language, when the rest of Haskell makes no such restrictions. I would expect, in a pure language, that I could reduce any expression in a program to HNF (or _|_) without affecting the meaning of the program. In Haskell, I can only do this so long as the expression doesn't involve any lazy I/O. 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? Cheers, Simon

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. Laziness makes computation demand-driven and allows us to abstract away the order of evaluation. We think this is useful (most of the time) and that's why Haskell is a lazy language. Demand-driven IO is useful too: sometimes, but not always, it is useful to abstract away from the exact details of when IO happens. For example, laziness should allow the following Haskell program to run in constant space, although it makes use of a large intermediate structure: main = let mylist = [1..10000] result = sum mylist in print result Now suppose that for some reason that instead of processing the large intermediate structure directly, I need to store it in a file and process it later, with a program like this: main = do let mylist = [1..10000] writeFile "myfile.txt" (unlines (map show mylist)) -- ... mylist' <- (map read . lines) `fmap` readFile "myfile.txt" let result = sum mylist' print result With lazy IO, this program too should run in constant space, and the functions that consume the large intermediate structure can remain unchanged! So, I think a lazy programming language should support not only lazy evaluation, but also lazy IO! When it is appropriate to abstract away from the details of when IO happens, lazy IO is useful. It can facilitate separation of concerns and modularity, just like lazy evaluation. Appropriate lazy IO should not compromise the purity of the language or be an obstacle to optimistic evaluation. Instead of settling for the monadic stopgap IO, we should continue to look for good ways to integrate laziness and IO! -- Thomas H "Delay is preferable to error." - Thomas Jefferson (3rd President of the United States)

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

Simon Marlow wrote:
So, we're agreed that the presence of lazy evaluation makes implementing optimistic evaluation (and other evaluation strategies) more complicated. Personally, I find it disturbing that the presence of this family of library functions affects something as low-level as the evaluation strategy of the language. Or, to put it another way, it enforces a particular evaluation strategy on a part of the language, when the rest of Haskell makes no such restrictions.
Lazy IO doesn't make lazy evaluation more complicated ;-) Optimistic evaluation is a already a complicated evaluation strategy and in my opinion lazy IO just makes it a bit more complicated. Lazy IO does not force you to use lazy evaluation any more than other uses of non-strictness force optimistic evaluation to bail out of eager evaluation.
I would expect, in a pure language, that I could reduce any expression in a program to HNF (or _|_) without affecting the meaning of the program. In Haskell, I can only do this so long as the expression doesn't involve any lazy I/O.
You always have to be careful with reducing an arbitrary expression in case it is _|_.
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?
I fear one problem is that the word "side effect" is not properly defined. Actually both getContents and interact are in the IO monad. Since the action "interact" is not terminated until the input stream ends, I do not see any problem there. Evaluation never leaves the IO monad until termination. getContents is more strange. However, you can just define its semantics to yield a random string. When you are not careful you may certainly get something nearly random ;-) I completely agree with Thomas Hallgren's message. I also view the IO monad as a temporary solution and regret that research into better lazy IO seems to have stoped. Olaf -- OLAF CHITIL, Dept. of Computer Science, The University of York, York YO10 5DD, UK. URL: http://www.cs.york.ac.uk/~olaf/ Tel: +44 1904 434756; Fax: +44 1904 432767

I completely agree with Thomas Hallgren's message. I also view the IO monad as a temporary solution and regret that research into better lazy IO seems to have stoped.
Well, not everywhere. Since noone else has mentioned it so far, it is worth throwing in the obligatory reference to the Software Technology department of the University of Nijmegen. "obligatory", because IO is perhaps the main area in which Clean has kept offering alternatives to Haskell, and recently, they have again taken the lead in this (IMHO). It is well worth browsing through their publication list, http://www.cs.kun.nl/st/Onderzoek/Publicaties/publicaties.html but perhaps a good starting point for this discussion is: Martijn Vervoort and Rinus Plasmeijer. Lazy Dynamic Input/Output in the lazy functional language Clean. To appear in Selected Papers Proceedings 14th International Workshop on the Implementation of Functional Languages, IFL 2002, Madrid, Spain, September 16-18, 2002. ftp://ftp.cs.kun.nl/pub/Clean/papers/2003/verm2003-LazyDynamicIO.ps.gz Cheers, Claus
participants (5)
-
Claus Reinke
-
Jan-Willem Maessen
-
Olaf Chitil
-
Simon Marlow
-
Thomas Hallgren