如何安全地通过 `System.IO.openFile` `mapM`

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

我的应用程序需要在运行时打开多个资源。我通过在应用程序开始时映射 openFile 一次来实现这一点。

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

此代码不安全,因为它可能适用于前 2 个文件路径,但在打开第 3 个文件路径时会抛出异常。在那种情况下,我需要关闭已经打开的前 2 个文件路径,这样我就可以在不泄漏资源的情况下退出该函数。我查看了 Control.Exception 中的函数和模式,但没有发现任何对这种情况有帮助的东西。我还没有看过 ResourceT。在这种情况下有帮助吗?

我想我正在寻找与此类似的函数签名:

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

其中 (b -> IO()) 是发生异常时调用的清理函数。

我能想到的解决方案可能不太好:

如果我没理解错的话,问题是在发生异常时如何保证安全关闭所有句柄。

对于单个文件,通常保证安全的方式是withFile。这里的复杂之处在于您要打开一系列文件。

也许我们可以编写这个辅助函数来执行 withFile 的嵌套分配,并将 Handle 的列表传递给最内层的回调:

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)

另一种方法是首先意识到我们正在做一些具有 replicateM 风格的事情:我们正在执行 n 次“效果”,并返回一个包含结果的列表。但是这里的“效果”(即 Applicative)是什么?它似乎是“使用确保释放的包装函数来保护资源的分配”。

这种效果似乎需要对“剩余计算”进行一些控制,因为当“剩余计算”以任何方式完成时,终结器必须仍然是运行。这将我们指向延续 monad 转换器,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

continuation transformer 对于这个目的来说可能有点太笼统了。像 managed 这样的库遵循相同的基本机制,但更侧重于资源处理。