13

I am trying to read the contents of a file, turn the text to upper-case and then write it back.

Here is the code I had written:

import System.IO
import Data.Char

main = do
    handle <- openFile "file.txt" ReadWriteMode
    contents <- hGetContents handle
    hClose handle
    writeFile "file.txt" (map toUpper contents)
    return ()

However, this writes nothing to the file, in fact, it even clears it.

I made some changes:

main = do
    handle <- openFile "file.txt" ReadWriteMode
    contents <- hGetContents handle
    writeFile "file.txt" (map toUpper contents)
    hClose handle
    return ()

However, I get the error resource busy (file is locked). How can I get this working and why it didn't work in both cases?

0

4 Answers 4

13

Lazy IO is bad, and this is generally considered to be a pain point in Haskell. Basically the contents isn't evaluated until you go to write it back to disk, at which point it can't be evaluated because the file is already closed. You can fix this in several ways, without resorting to extra libraries you can use the readFile function and then check the length before writing back out:

import Control.Monad (when)

main = do
    contents <- readFile "file.txt"
    let newContents = map toUpper contents
    when (length newContents > 0) $
        writeFile "file.txt" newContents

I would say this code is actually better anyway because you don't write back out to a file that is already empty, a pointless operation.

Another way would be to use a streaming library, pipes is a popular choice with some good tutorials and a solid mathematical foundation, and that would be my choice as well.

Sign up to request clarification or add additional context in comments.

6 Comments

And the second attempt failed, because OP tried to open a file for writing, which was already opened for writing (because of ReadWriteMode)
Anton Guryanov: in my experiments, it failed even when the file was just opend with ReadMode.
By calling length on newContents, you are forcing newContents to be evaluated completely before you write the file. Is that correct?
@bwroga length newContents won't force anything by itself, it's when we force the evaluation of the entire expression length newContents > 0 that newContents gets evaluated.
And because the first argument to when is evaluated before the second?
|
6

I think your problem is that hGetContents is lazy. The contents of the file are not immediately read in when you use hGetContents. They are read in when they are needed.

In your first example you open the file and say that you want the contents, but then close the file before you do anything with them. Then you write to the file. When you write to an existing file the contents are cleared, but since you closed the file you no longer have access to the file contents.

In the second example you open the file and then try to write to the file, but because the contents are not really read until they are needed (when they are transformed and written back) you end up trying to write to and read from the same file at the same time.

You could write to a file named file2.txt then when you are done, delete file.txt and rename file2.txt to file.txt

4 Comments

You've correctly pointed out the problem, but your solution is needlessly complicated.
@leftaroundabout Is bheklilr's approach what you would recommend?
Yes, readFile is what I'd recommend unless there's a reason not to (performance etc.), or you're already using conduit or pipes in the project.
The write-to-new-file-and-rename approach is actually better anyways, since it doesn't risk losing data in the case of a crash in the middle of the write.
5

There's a couple things going on here

You opened the file in ReadWriteMode, but only read the contents. Why not use the same handle for both?

main = do
    handle <- openFile "file.txt" ReadWriteMode
    contents <- hGetContents' handle
    hSeek handle AbsoluteSeek 0
    hPutStr handle (map toUpper contents)
    hClose handle
    return ()

hGetContents will put the handle in a semi-closed state, so you'll need something else to read the file contents:

hGetContents' :: Handle -> IO String
hGetContents' h = do
  eof <- hIsEOF h
  if eof
    then
      return []
    else do
      c <- hGetChar h
      fmap (c:) $ hGetContents' h

6 Comments

Please don't recommend kludges to get Handles to work, without also mentioning some of the better alternatives. FWIW, hGetContents'would better be implemented with deepseq (it's in fact the example on that page).
or just hGetContents' h = do { c <- hGetContents h ; if (length c > 0) then return c else return "" }.
Will Ness: that doesn't prevent the handle from being put in a semi-closed state
yes, but the returned c contains contents of the whole file already, length forces it through to the end, so there's nothing more to read from the file. I tested it with c <- hGetContents' h; hClose h; ... use c... and it worked.
Will Ness: while that's a valid answer to OP's question, my answer's about using a single handle for read and write, so I don't see the relevance.
|
5

@bwroga's answer is perfectly correct. Here is an implementation of the suggested approach (writing to temporary file & renaming):

import Data.Char (toUpper)
import System.Directory (renameFile, getTemporaryDirectory)
import System.Environment (getArgs)

main = do
    [file] <- getArgs
    tmpDir <- getTemporaryDirectory
    let tmpFile = tmpDir ++ "/" ++ file
    readFile file >>= writeFile tmpFile . map toUpper
    renameFile tmpFile file

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.