Handling Files more Easily

Earlier this week we learned about the Handle abstraction which helps us to deal with files in Haskell. An important part of this abstraction is that handles are either "open" or "closed". Today we'll go over a couple ideas to help us deal with opening and closing handles more gracefully.

When we first get a handle, it is "open". Once we're done with it, we can (and should) "close" it so that other parts of our program can use it safely. We close a handle with the hClose function:

hClose :: Handle -> IO ()

Most of the time I use files, I find myself opening and closing the handle in the same function. So for example, if we're reading a file and getting the first 4 lines:

read4Lines :: FilePath -> IO [String]
read4Lines fp = do
  handle <- openFile fp ReadMode
  myLines <- lines <$> readFile handle
  let result = take 4 myLines
  hClose handle
  return myLines

A recommendation I would give when you are writing a function like this is to write the code for the hClose call immediately after you write the code of openFile. So your might start like this:

read4Lines :: FilePath -> IO [String]
read4Lines fp = do
  handle <- openFile fp ReadMode
  -- Handle logic
  hClose handle
  -- Return result

And only after writing these two lines should you write the core logic of the function and the final return statement. This is an effective way to make sure you don't forget to close your handles.

In the strictest sense though, this isn't even fool proof. If you cause some kind of exception while performing your file operations, an exception will be thrown before your program closes the handle. The technically correct way out of this is to use the bracket pattern. The bracket function allows you to specify an action that will take place after the main action is done, no matter if an exception is thrown. This is like using try/catch/finally in Java or try/except/finally in Python. The finally action is the second input to bracket, while our main action is the final argument.

bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c

If we specialize this signature to our specific use case, it might look like this:

bracket :: IO Handle -> (Handle -> IO ()) -> (Handle -> IO [String]) -> IO [String]

And we can then write our function above like this:

read4LinesHandle :: Handle -> IO [String]
read4LinesHandle handle = do
  myLines <- lines <$> readFile handle
  let result = take 4 myLines
  return result

read4Lines :: FilePath -> IO [String]
read4Lines fp = bracket (openFile fp) hClose read4LinesHandle

Now our handle gets closed even if we encounter an error while reading.

Now, this pattern (open a file, perform a handle operation, close the handle) is so common with file handles specifically that there's a special function for it: withFile:

withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r

This makes our lives a little simpler in the example above:

read4LinesHandle :: Handle -> IO [String]
read4LinesHandle handle = ...

read4Lines :: FilePath -> IO [String]
read4Lines fp = withFile fp ReadMode read4LinesHandle

If you're ever in doubt about whether a Handle is open or not, you can also check this very easily. There are a couple boolean functions to help you out:

hIsOpen :: Handle -> IO Bool

hIsClosed :: Handle -> IO Bool

Hopefully this gives you more confidence in the proper way to deal with file handles! We'll be back next week with some more tricks you can do with these objects. In the meantime, you can subscribe to our monthly newsletter! This will keep you up to date with our latest articles and give you access to our subscriber resources!

Previous
Previous

Finding What You Seek

Next
Next

Getting a Handle on IO