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!