views:

328

answers:

3

Hi,

Still quite new to Haskell..

I want to read the contents of a file, do something with it possibly involving IO (using putStrLn for now) and then write new contents to the same file.

I came up with:

doit :: String -> IO ()
doit file = do
    contents <- withFile tagfile ReadMode $ \h -> hGetContents h
    putStrLn contents
    withFile tagfile WriteMode $ \h -> hPutStrLn h "new content"

However this doesn't work due to laziness. The file contents are not printed. I found this post which explains it well.

The solution proposed there is to include putStrLn within the withFile:

doit :: String -> IO ()
doit file = do
    withFile tagfile ReadMode $ \h -> do
        contents <- hGetContents h
        putStrLn contents
    withFile tagfile WriteMode $ \h -> hPutStrLn h "new content"

This works, but it's not what I want to do. The operation in I will eventually replace putStrLn might be long, I don't want to keep the file open the whole time. In general I just want to be able to get the file content out and then close it before working with that content.

The solution I came up with is the following:

doit :: String -> IO ()
doit file = do
    c <- newIORef ""
    withFile tagfile ReadMode $ \h -> do
        a <- hGetContents h
        writeIORef c $! a
    d <- readIORef c
    putStrLn d
    withFile tagfile WriteMode $ \h -> hPutStrLn h "Test"

However, I find this long and a bit obfuscated. I don't think I should need an IORef just to get a value out, but I needed "place" to put the file contents. Also, it still didn't work without the strictness annotation $! for writeIORef. I guess IORefs are not strict by nature?

Can anyone recommend a better, shorter way to do this while keeping my desired semantics?

Thanks!

A: 

It's ugly but you can force the contents to be read by asking for the length of the input and seq'ing it with the next statement in your do-block. But really the solution is to use a strict version of hGetContents. I'm not sure what it's called.

Martijn
A: 

I have answered this before. I think it will answer your question.

http://stackoverflow.com/questions/296792/haskell-io-and-closing-files/297630#297630

luqui
@luqui: I read your other post. Is the key idea that you have to avoid `withFile`, because it closes the handle before you can force the I/O to take place?
Norman Ramsey
+2  A: 

The reason your first program does not work is that withFile closes the file after executing the IO action passed to it. In your case, the IO action is hGetContents which does not read the file right away, but only as its contents are demanded. By the time you try to print the file's contents, withFile has already closed the file, so the read fails (silently).

You can fix this issue by not reinventing the wheel and simply using readFile and writeFile:

doit file = do
    contents <- readFile file
    putStrLn contents
    writeFile file "new content"

But suppose you want the new content to depend on the old content. Then you cannot, generally, simply do

doit file = do
    contents <- readFile file
    writeFile file $ process contents

because the writeFile may affect what the readFile returns (remember, it has not actually read the file yet). Or, depending on your operating system, you might not be able to open the same file for reading and writing on two separate handles. The simple but ugly workaround is

doit file = do
    contents <- readFile file
    length contents `seq` (writeFile file $ process contents)

which will force readFile to read the entire file and close it before the writeFile action can begin.

Reid Barton
Having read the post you link to, I guess you already know some of this. But since there is more than one laziness issue here I thought it best to be thorough.
Reid Barton
Rather than using 'length contents `seq` ...'. I think you could use the BangPatterns extension and rewrite the previous line as '!contents <- readFile file'.
Alasdair
That is equivalent to 'contents `seq` ...', which is not enough: it will only evaluate the top-level constructor of contents (i.e. whether it is empty) which means only the first chunk of the file will be read.
Reid Barton