Rationale
I came across the following scenario: Given a list of resources, initialize all these resources, and give me a list of initialized handles. Think of a list of files. You want to open all the files at once, and run a handler:
= do
main let logAndOpen fn = do
putStrLn $ "opening " <> pack fn
ReadMode
openFile fn = do
logAndClose handle putStrLn "closing handle"
"/tmp/foo.txt", "/tmp/baz.txt", "/tmp/bar.txt"] logAndOpen logAndClose \fileList -> do
bracketList [putStrLn "opened files"
<- hGetLine (head fileList)
line putStrLn $ "first line, first file: " <> pack line
Here, we open three files, /tmp/foo.txt
, /tmp/baz.txt
, /tmp/bar.txt
, and just read a line from the first handle we get.
The function bracketList
is assumed to already exist, taking a list of input values, an acquire
function (in our case logAndOpen
), a release
function (logAndClose
) as well as a function that processes the initialized list. This mimicks the bracket
function in Control.Exception
. I was a bit confused that the function I was seeking didn’t seem to already exist. I’m sure I missed it, but since I had a bit of time, I decided to write one myself.
First attempt
We’re after the following function:
bracketList :: [a] -> (a -> IO b) -> (b -> IO c) -> ([b] -> IO d) -> IO d
= undefined bracketList inputList acquire release withList
Ideally, we’d like to use the existing bracket
:
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
Let’s first try with foldl'
:
-- foldl' :: Foldable t => (b -> a -> b) -> b -> t a -> b
= foldl' folder [] inputList bracketList inputList acquire release withList
Where
folder :: [b] -> a -> [b]
But this doesn’t work. We have to execute acquire
, which is of type a -> IO b
, so we have to account for IO
. So maybe foldM
from Control.Monad
?
foldM :: (Foldable t) => (b -> a -> IO b) -> b -> t a -> IO b
Let’s try:
= foldM folder [] inputList >>= withList bracketList inputList acquire release withList
with
folder :: [b] -> a -> IO [b]
=
folder alreadyInited newElement
bracket
(acquire newElement)
release-> pure (acquiredElement : alreadyInited)) (\acquiredElement
This compiles and even runs. But it doesn’t do the right thing. This will acquire the resources, release them, and then give them to the callback. Think of file handles again: we get the file handle list, close the file handles, and then call the callback to do something on the list? Doesn’t make sense. In the example code, we simply get:
/tmp/bar.txt: hGetLine: illegal operation (handle is closed)
Second, and final attempt
So, instead of using bracket
, we do the bracketing ourselves:
bracketList :: forall a b c d. [a] -> (a -> IO b) -> (b -> IO c) -> ([b] -> IO d) -> IO d
=
bracketList inputList acquire release withList let folder :: Either IOException [b] -> a -> IO (Either IOException [b])
Left e) _ = pure (Left e)
folder (Right alreadyInited) newElement = do
folder (>>= \newElt -> pure (Right (newElt : alreadyInited))) `catch` \e -> do
(acquire newElement
traverse_ release alreadyInitedpure (Left e)
in do
<- foldM folder (Right []) inputList
resultListOrError case resultListOrError of
Left e -> throw e
Right resultList -> finally (withList (reverse resultList)) (traverse_ release resultList)
As you can see, we still use foldM
, but this time short-circuiting if we encounter an error (meaning, no further elements are processed), returning either the first error that occurred, or a list of initialize values.
If we receive an error during initialization, we catch
it and recover by cleaning up and returning the error as pure (Left e)
.
Note that this assumes the release
function not to throw.
Does it work? Let’s test, assuming /tmp/baz.txt
doesn’t exist, and the other two files do:
= do
main let logAndOpen fn = do
putStrLn $ "opening " <> pack fn
ReadMode
openFile fn = do
logAndClose handle putStrLn "closing handle"
"/tmp/foo.txt", "/tmp/baz.txt", "/tmp/bar.txt"] logAndOpen logAndClose \fileList -> do
bracketList [putStrLn "opened files"
<- hGetLine (head fileList)
line putStrLn $ "first line, first file: " <> pack line
This outputs:
opening /tmp/foo.txt
opening /tmp/baz.txt
closing handle
main: /tmp/baz.txt: openFile: does not exist (No such file or directory)
which is just what we wanted.