Haskell’s System.IO.withFile
has a nice resource safety property.
It also has a problem which its Bluefin equivalent fixes. Let’s have
a look.
withFile
System.IO.withFile
has the following type
withFile ::
FilePath ->
IOMode ->
-- | The "body"
Handle -> IO r) ->
(IO r
It allows us to open a file by its path, and then interact with it in
the “body” of the withFile
using its “file handle” (of type
System.IO.Handle
). When
the body has finished withFile
will automatically close the file,
releasing its Handle
. File handles are a scarce resource so it’s
important to release them promptly. withFile
saves us work by
taking care of that for us automatically. Here’s an example
useHandle :: IO ()
= do
useHandle "/tmp/message" WriteMode $ \handle -> do
System.IO.withFile -- This do block is called the "body" of the withFile
"Hello!"
System.IO.hPutStrLn handle
-- handle has been closed by the time we get here
Even better, withFile
is “bracketed”. That means that it releases
its Handle
even when the body it terminated by an exception:
useHandleException :: IO ()
= do
useHandleException "/tmp/message" WriteMode $ \handle -> do
System.IO.withFile -- This do block is called the "body" of the withFile
"Hello!"
System.IO.hPutStrLn handle error "Oh dear!"
-- handle has been closed by the time we get here,
-- even though the body was terminated by an exception
But all is not good. Nothing stops us leaking the handle from
withFile
and attempting to access it after it has been closed. For
example, the following code errors out at run time:
leakHandle :: IO ()
= do
leakHandle <-
handle "/tmp/output-file" WriteMode $ \handle -> do
System.IO.withFile pure handle
"Still open?" System.IO.hPutStrLn handle
ghci> leakHandle
*** Exception: /tmp/output-file: hPutStr: illegal operation (handle is closed)
Bluefin’s version of
withFile
doesn’t suffer from the same problem. If we try to write code that
leaks the handle from withFile
we get a type error.
leakHandleBf :: IO ()
= runEff $ \io -> do
leakHandleBf <-
handle
Bluefin.System.IO.withFile
io"/tmp/output-file"
WriteMode
$ \handle -> do
pure handle
"Still open?" Bluefin.System.IO.hPutStrLn handle
/home/tom/H2/code/logger.hs:208:10: error:
• Couldn't match type ‘e0’ with ‘e1’
Expected: Handle e0
Actual: Handle e1
• because type variable ‘e1’ would escape its scope
How does that happen? It’s because of the type of Bluefin’s
withFile
, which is:
withFile ::
:> es) =>
(e1 IOE e1 ->
FilePath ->
IOMode ->
forall e. Handle e -> Eff (e :& es) r) ->
(Eff es r
The type of the handle that Bluefin’s withFile
provides to its body
is Handle e
(that’s a
Bluefin.System.IO.Handle
),
i.e. it is tagged with the Bluefin effect type e
. The forall
that
binds e
prevents us from smuggling the handle out as part of r
,
the return type of the block. This technique is an augmented version
of the one used by
runST
to avoid leaking STRef
s.
So System.IO.withFile
has a neat resource safety property, and
Bluefin’s equivalent is even safer!