How do I read (and parse) a file and then append to the same file without getting an exception?

后端 未结 2 1804
情深已故
情深已故 2021-01-03 07:47

I am trying to read from a file correctly in Haskell but I seem to get this error.

*** Exception: neo.txt: openFile: resource busy (file is locked) This is my code.<

2条回答
  •  臣服心动
    2021-01-03 08:43

    Whoopsy daisy, being lazy
    tends to make file changes crazy.
    File's not closed, as supposed
    thus the error gets imposed.
    This small guile, by loadFile
    is what you must reconcile.
    But don't fret, least not yet,
    I will show you, let's get set.


    As many other functions that work with IO in System.IO, readFile doesn't actually consume any input. It's lazy. Therefore, the file doesn't get closed, unless all its content has been consumed (it's then half-closed):

    The file is read lazily, on demand, as with getContents.

    We can demonstrate this on a shorter example:

    main = do
      let filename = "/tmp/example"
      writeFile filename "Hello "
      contents <- readFile filename
      appendFile filename "world!"   -- error here
    

    This will fail, since we never actually checked contents (entirely). If you get all the content (for example with printing, length or similar), it won't fail anymore:

    main = do
      let filename = "/tmp/example2"
      writeFile filename "Hello "
      content <- readFile filename
      putStrLn content
      appendFile filename "world!"   -- no error
    

    Therefore, we need either something that really closes the file, or we need to make sure that we've read all the contents before we try to append to the file.

    For example, you can use withFile together with some "magic" function force that makes sure that the content really gets evaluated:

    readFile' filename = withFile filename ReadMode $ \handle -> do
      theContent <- hGetContents handle
      force theContent
    

    However, force is tricky to achieve. You could use bang patterns, but this will evaluate the list only to WHNF (basically just the first character). You could use the functions by deepseq, but that adds another dependency and is probably not allowed in your assignment/exercise.

    Or you could use any function that will somehow make sure that all elements are evaluated or sequenced. In this case, we can use a small trick and mapM return:

    readFile' filename = withFile filename ReadMode $ \handle -> do
      theContent <- hGetContents handle
      mapM return theContent
    

    It's good enough, but you would use something like pipes or conduit instead in production.

    The other method is to make sure that we've really used all the contents. This can be done by using another parsec parser method instead, namely runParserT. We can combine this with our withFile approach from above:

    parseFile :: ParsecT String () IO a -> FilePath -> IO (Either ParseError a) 
    parseFile p filename = withFile filename ReadMode $ \handle ->
      hGetContents handle >>= runParserT p () filename
    

    Again, withFile makes sure that we close the file. We can use this now in your loadFilm:

    loadFile :: FilePath -> IO (Either ParseError [Film])
    loadFile filename = parseFile films filename
    

    This version of loadFile won't keep the file locked anymore.

提交回复
热议问题