Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.
like image 355
m1-s Avatar asked Aug 19 '21 08:08

m1-s


People also ask

What is System Io memorymappedfiles?

System.IO.MemoryMappedFiles Namespace. The System.IO.MemoryMappedFiles namespace provides classes for using a memory-mapped file, which maps the contents of a file to an application's logical address space. Represents a memory-mapped file. Represents the permissions that can be granted for file access and operations on memory-mapped files.

Can memory-mapped files be associated with an optional name?

A memory-mapped file can be associated with an optional name that allows the memory-mapped file to be shared with other processes. You can create multiple views of the memory-mapped file, including views of parts of the file.

What happens if map_fixed is used for a specific page?

The file remains referenced as long as a mapping exists over the file. If a mapping already exists for the portion of the processes address space that is to be mapped and the value MAP_FIXED was specified for flags , then the previous mappings for the affected pages are implicitly unmapped.

What is an memory mapped file?

Memory Mapped Files Some information relates to prerelease product that may be substantially modified before it’s released. Microsoft makes no warranties, express or implied, with respect to the information provided here. Represents a memory-mapped file.


Video Answer


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.

like image 118
danidiaz Avatar answered Oct 17 '22 21:10

danidiaz