如何在`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 的库遵循相同的基本机制,但更侧重于资源处理。


以上是如何在`System.IO.openFile`上安全地`mapM`的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>