Learn You a Haskell for Great Good! - Miran Lipovaca [78]
Note the difference between a handle and the actual contents of the file. A handle just points to our current position in the file. The contents are what’s actually in the file. If you imagine your whole filesystem as a really big book, the handle is like a bookmark that shows where you’re currently reading (or writing).
With putStr contents, we print the contents out to the standard output, and then we do hClose, which takes a handle and returns an I/O action that closes the file. You need to close the file yourself after opening it with openFile! Your program may terminate if you try to open a file whose handle hasn’t been closed.
Using the withFile Function
Another way of working with the contents of a file as we just did is to use the withFile function, which has the following type signature:
withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
It takes a path to a file, an IOMode, and a function that takes a handle and returns some I/O action. Then it returns an I/O action that will open that file, do something with the file, and close it. Furthermore, if anything goes wrong while we’re operating on our file, withFile makes sure that the file handle gets closed. This might sound a bit complicated, but it’s really simple, especially if we use lambdas.
Here’s our previous example rewritten to use withFile:
import System.IO
main = do
withFile "girlfriend.txt" ReadMode (\handle -> do
contents <- hGetContents handle
putStr contents)
(\handle -> ...) is the function that takes a handle and returns an I/O action, and it’s usually done like this, with a lambda. It needs to take a function that returns an I/O action, rather than just taking an I/O action to do and then closing the file, because the I/O action that we would pass to it wouldn’t know on which file to operate. This way, withFile opens the file and then passes the handle to the function we gave it. It gets an I/O action back from that function and then makes an I/O action that’s just like the original action, but it also makes sure that the file handle gets closed, even if something goes awry.
It's Bracket Time
Usually, if a piece of code calls error (such as when we try to apply head to an empty list) or if something goes very wrong when doing input and output, our program terminates, and we see some sort of error message. In such circumstances, we say that an exception gets raised. The withFile function makes sure that despite an exception being raised, the file handle is closed.
This sort of scenario comes up often. We acquire some resource (like a file handle), and we want to do something with it, but we also want to make sure that the resource gets released (for example, the file handle is closed). Just for such cases, the Control.Exception module offers the bracket function. It has the following type signature:
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
Its first parameter is an I/O action that acquires a resource, such as a file handle. Its second parameter is a function that releases that resource. This function gets called even if an exception has been raised. The third parameter is a function that also takes that resource and does something with it. The third parameter is where the main stuff happens, like reading from a file or writing to it.
Because bracket is all about acquiring a resource, doing something with it, and making sure it gets released, implementing withFile is really easy:
withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
withFile name mode f = bracket (openFile name mode)
(\handle -> hClose handle)
(\handle -> f handle)
The first parameter that we pass to bracket opens the file, and its result is a file handle. The second parameter takes that