Chapter 7: hazards of lazy IO

8 views
Skip to first unread message

Chip Grandits

unread,
Feb 22, 2009, 2:49:35 PM2/22/09
to Real World Haskell Book Club
There are two "bear trap" warnings one right after the other on page
179 and 180 about the hazards on not understanding lazy IO. I want to
make sure that I specifically understand the hazards of referring to
variables bound by lazy IO operations (actions?) on page 180.

The warning says "...you must not close the Handle until you have
finished consuming its results..."

I have modified toupper-lazy2.hs to intentionally make such a mistake,
to make sure I understand.

{-- file toupper-bad.hs--}
import System.IO
import Data.Char(toUpper)

main = do
inh <- openFile "input.txt" ReadMode
outh <- openFile "output.txt" WriteMode
inpStr <- hGetContents inh
hPutStr outh (map toUpper inpStr)
hClose inh
let len = length inpStr --Oops! inpStr bound using lazy
IO
putStrLn $ show len ++ " characters processed."
hClose outh
{-- end file toupper-bad.hs--}

So have I written a Haskell program that might give a different answer
for the same input file depending upon the Haskell compiler,
environment, operating system and/or other processes going on? Or
will the compiler cleverly cover up this mistake?

Here the mistake is obvious, since it is deliberate, and part of an
example. However I can see where in more complex projects, this kind
of mistake might be subtle. ghci happily loads this program with
comment; if this code is problematic than I ask, are there tools to
protect us from our bungling?

John Goerzen

unread,
Feb 24, 2009, 1:16:38 PM2/24/09
to real-world-has...@googlegroups.com
Chip Grandits wrote:
> Here the mistake is obvious, since it is deliberate, and part of an
> example. However I can see where in more complex projects, this kind
> of mistake might be subtle. ghci happily loads this program with
> comment; if this code is problematic than I ask, are there tools to
> protect us from our bungling?

I assume you meant *without* comment.

As far as I know, there are no tools to help with this. However, the
usual pattern when using getContents is to pass the String directly to
pure code. That is, the part of the code in the IO monad will be pretty
small and easily debuggable.

-- John

Chip Grandits

unread,
Feb 24, 2009, 6:06:36 PM2/24/09
to Real World Haskell Book Club
Yes I did mean to say "without comment"

Thanks for your response.

The consensus in the Colorado study group was to favor the use the
"with-File" idiom, as in the example function withTempFile in Ch 7.
This is basically as you suggested - thanks for the re-reinforcement.

However as long as I'm re-posting
There is an additional subtle issue in that I'm not sure if its really
wrong.
Let me contrast with an example I know for sure is wrong

{-- easy to verify this is bad--}
main = do
inh <- openFile "input.txt" ReadMode
outh <- openFile "output.txt" WriteMode
inpStr <- hGetContents inh
hClose inh --definitely too soon!!!
hPutStr outh (map toUpper inpStr)
let len = length inpStr
...

Every time I run this len is zero, when input.txt is not zero length.
Here the laziness is easily exposed.

{--more subtle, not clear if output code is also lazy?--}
main = do
inh <- openFile "input.txt" ReadMode
outh <- openFile "output.txt" WriteMode
inpStr <- hGetContents inh
--does this line of code force evaluation of inpStr?
hPutStr outh (map toUpper inpStr)
hClose inh --not sure if too soon?
let len = length inpStr
...

This gave the correct answer on my machine
(although I did not try with really gargantuan size input.txt)

I mean it's obviously bad style, but I'm not clear if
map toUpper inpStr
is guaranteed to force the evaluation of all of inpStr,
or if
hPutStr
is what finally forced it
or if either or both question are implementation dependent.

It's not really a critical question, more just a curiosity for
understanding laziness.

Thanks again, great book! - my copy is already becoming dog-eared!

-Chip

John Goerzen

unread,
Feb 24, 2009, 6:34:43 PM2/24/09
to real-world-has...@googlegroups.com
On Tue, Feb 24, 2009 at 03:06:36PM -0800, Chip Grandits wrote:
> main = do
> inh <- openFile "input.txt" ReadMode
> outh <- openFile "output.txt" WriteMode
> inpStr <- hGetContents inh
> --does this line of code force evaluation of inpStr?
> hPutStr outh (map toUpper inpStr)
> hClose inh --not sure if too soon?
> let len = length inpStr
> ...
>
> This gave the correct answer on my machine
> (although I did not try with really gargantuan size input.txt)
>
> I mean it's obviously bad style, but I'm not clear if
> map toUpper inpStr
> is guaranteed to force the evaluation of all of inpStr,
> or if
> hPutStr
> is what finally forced it
> or if either or both question are implementation dependent.

So hPutStr is what really forced it, because it demanded the result of
(map toUpper inpStr) to be calculated. Had you just said

let foo = map toUpper inpStr

the input would not have been evaluated. Nothing is evaluated until
it must be, and hPutStr required it.

The reason that (length inpStr) produced the correct result is this:
when inpStr was evaluated as part of the hPutStr call, it was not yet
garbage-collected because inpStr remained in scope for the later call
to length. By the time length is called, the entire input had, of
course, already been consumed during the call to hPutStr and was being
held in memory. So this program would not be able to process a 1GB
input file.

Hope that clarifies.

-- John

Jay True

unread,
Feb 24, 2009, 7:56:00 PM2/24/09
to real-world-has...@googlegroups.com
On my machine the first example gives no output in output.txt file.

2009/2/25 Chip Grandits <Bould...@gmail.com>



--
歌词唱清楚,不是周杰伦
Reply all
Reply to author
Forward
0 new messages