如何在`System.IO.openFile`上安全地`mapM`
我的应用程序需要在运行时打开多个资源。我通过openFile在应用程序开始时映射一次来实现这一点。
let filePaths = ["/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2"]
fileHandles <- mapM (`openFile` ReadWriteMode) filePaths
此代码不安全,因为它可能适用于前 2 个文件路径,但在打开第三个文件路径时会引发异常。在这种情况下,我需要关闭已经打开的前 2 个文件路径,这样我就可以退出该函数而不会泄漏资源。我查看了其中的函数和模式,Control.Exception但没有发现任何对这种情况有帮助的内容。我还没有看过ResourceT。在这种情况下它有帮助吗?
我想我正在寻找与此类似的函数签名:
safeMapM:: [a] -> (a -> IO b) -> (b -> IO()) -> [b]
(b -> IO())发生异常时调用的清理函数在哪里。
我能想到的解决方案可能不好:
- 将每个元素包装在一个
Maybe. 可以捕获异常并导致Nothing. 在mapM总能完成,我可以事后检查没什么/异常,并通过他们,然后关闭所有成功打开的文件句柄Just Handle。 - 使用折叠代替地图。当当前元素发生异常时,我可以关闭折叠的所有先前元素的文件句柄,然后重新抛出异常以阻止折叠继续。
回答
如果我理解正确,问题是如何确保在发生异常时安全关闭所有句柄。
对于单个文件,通常确保安全的方法是withFile. 这里的复杂之处在于您想要打开一系列文件。
也许我们可以编写这个辅助函数来执行嵌套分配withFile并将Handles列表传递给最内层的回调:
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这里的“效果”(即 )是什么?它似乎是“使用确保释放的包装函数来保护资源的分配”。
这种效果似乎需要对“其余计算”进行一些控制,因为当“其余计算”以任何方式完成时,仍必须运行终结器。这将我们指向了 continuation 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
对于这个目的,延续变压器可能有点过于通用了。有一些类似managed 的库遵循相同的基本机制,但更侧重于资源处理。