
Magnus Therning:
Just out of curiosity, how would I go about finding this myself? (Ideally it'd be an answer other than "read the source for the libraries you are using". :-)
Well, I can at least try to expand a little on "read the source". :-) You'll first need a solid understanding of lazy evaluation in the context of pure computations. Read about normal evaluation order, WHNF (weak head normal form), and which contexts force WHNF. Use pen and paper to manually derive the evaluation order for some pure computations of your choice (folds over infinite lists would be a good start). Experiment with "seq". Experiment with the various causes of stack overflows. Next, understand that while the IO monad is quite strict about sequencing actions, its "return" is not strict in its argument. Observe that the combinators in Control.Monad generally do not force returned computations. Browse the source of GHC's IO library to understand how it sequences IO actions. Read the source for unsafePerformIO and unsafeInterleaveIO to understand how and for what purposes they allow you to break that sequencing. Next, read the source for hGetContents. After all that, you should be a little better equipped to diagnose problems with lazy IO! Magnus:
Another question that came up when talking to a (much more clever) colleague was whether the introduction of either of the solutions in fact means that only a single file is open at any time?
You program is single-threaded, so it's probably safe to conclude that either: a) it has only one file open at a time, or b) it has all the files open at once, or c) leaked handles are being garbage collected or are insufficient to cause exhaustion. Perhaps you can use your newly discovered understanding of lazy IO to select the correct answer. :-)