How to safely `mapM` over `System.IO.openFile`

Viewed 165

My application needs to have several resources open during runtime. I am achieving this by mapping over openFile once at the start of the application.

let filePaths = ["/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2"]
fileHandles <- mapM (`openFile` ReadWriteMode) filePaths

This code is not safe as it might work for the first 2 file paths but then throw an exception when opening the 3rd file path. In that case i need to close the first 2 file paths that were opened already so i can exit the function without leaking resources. i have looked at the functions and patterns from Control.Exception but not found anything that helps in this case. I have not looked at ResourceT yet. Does it help in this situation?

I think i am looking for a function signature that is similar to this:

safeMapM:: [a] -> (a -> IO b) -> (b -> IO()) -> [b]

where (b -> IO()) is the clean up function that is called when some exception occurred.

Solutions i could think of that are probably not good:

  • wrapping each element in a Maybe. Exceptions can be catched and result in a Nothing. the mapM could always finish and i could check for Nothing/Exception afterwards and then close all successfully opened file handles by their Just Handle.
  • using fold instead of map. when an exception at the current element occurs i can close the file handles of all previous elements of the fold and then rethrow the exception to stop the fold from continuing.
1 Answers

If I understand correctly, the problem is how to ensure safe closing of all handles when an exception happens.

For a single file, the usual way of ensuring safety is withFile. The complication here is that you want to open a sequence of files.

Perhaps we could write this auxiliary function that performs nested allocations of withFile and passes the list of Handles to a callback in the innermost level:

nestedWithFile :: [FilePath] -> IOMode -> ([Handle] -> IO r) -> IO r
nestedWithFile filePaths mode callback = go [] filePaths
  where
  go acc [] = 
    callback acc -- innermost invocation, protected by the withFiles
  go acc (p : ps)  = 
    withFile p mode (\handle -> go (acc ++ [handle]) ps)

Another way of doing it begins by realizing that we are doing something with a replicateM flavor: we are performing an "effect" n times, and returning a list with the results. But what would be the "effect" (that is, the Applicative) here? It seems to be "protecting the allocation of a resource with a wrapper function that ensures release".

This kind of effect seems to require some control over the "rest of the computation", because when the "rest of the computation" finishes in any way, the finalizer must still be run. This points us to the continuation monad transformer, ContT:

import Control.Monad
import Control.Monad.Trans.Cont
import System.IO

openFile' :: FilePath -> ContT r IO Handle
openFile' filePath = ContT (withFile filePath ReadWriteMode)

openSameFileSeveralTimes :: Int -> FilePath -> ContT r IO [Handle]
openSameFileSeveralTimes count filePath = replicateM count (openFile' filePath)

-- The handles are freed when the ([Handle] -> IO r) callback exits
useHandles :: ContT r IO [Handle] -> ([Handle] -> IO r) -> IO r
useHandles = runContT

The continuation transformer might be a bit too general for this purpose. There are libraries like managed which follow the same basic mechanism but are more focused on resource handling.

Related