如何安全地通过 `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())
是发生异常时调用的清理函数。
我能想到的解决方案可能不太好:
- 将每个元素包装在
Maybe
中。可以捕获异常并导致 Nothing
。 mapM
总是可以完成,我可以在之后检查 Nothing/Exception,然后通过 Just Handle
. 关闭所有成功打开的文件句柄
- 使用折叠而不是地图。当当前元素发生异常时,我可以关闭折叠中所有先前元素的文件句柄,然后重新抛出异常以停止折叠继续。
如果我没理解错的话,问题是在发生异常时如何保证安全关闭所有句柄。
对于单个文件,通常保证安全的方式是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 这样的库遵循相同的基本机制,但更侧重于资源处理。
我的应用程序需要在运行时打开多个资源。我通过在应用程序开始时映射 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())
是发生异常时调用的清理函数。
我能想到的解决方案可能不太好:
- 将每个元素包装在
Maybe
中。可以捕获异常并导致Nothing
。mapM
总是可以完成,我可以在之后检查 Nothing/Exception,然后通过Just Handle
. 关闭所有成功打开的文件句柄
- 使用折叠而不是地图。当当前元素发生异常时,我可以关闭折叠中所有先前元素的文件句柄,然后重新抛出异常以停止折叠继续。
如果我没理解错的话,问题是在发生异常时如何保证安全关闭所有句柄。
对于单个文件,通常保证安全的方式是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 这样的库遵循相同的基本机制,但更侧重于资源处理。