The signature of modifyIORef
is straightforward enough:
modifyIORef :: IORef a -> (a -> a) -> IO ()
Unfortunately, this is not thread safe. There is an alternative that adresses this issue:
atomicModifyIORef :: IORef a -> (a -> (a,b)) -> IO b
What exactly are the differences between these two functions? How am I supposed to use the b
parameter when modifying an IORef
that might be read from another thread?
As you stated in a comment, without concurrency you'd be able to just write something like
modifyAndReturn ref f = do
old <- readIORef ref
let !(new, r) = f old
writeIORef r new
return r
But in a concurrent context, someone else could change the reference between the read and the write.
The extra parameter is used to provide a return value. For example, you may want to be able to atomically replace the value stored in a IORef
and return the old value. You can do that like so:
atomicModifyIORef ref (\old -> (new, old))
If you don't have a value to return, you can use the following:
atomicModifyIORef_ :: IORef a -> (a -> a) -> IO ()
atomicModifyIORef_ ref f =
atomicModifyIORef ref (\val -> (f val, ()))
which has the same signature as modifyIORef
.
Here's how I understand this. Think of functions that follow the bracket idiom, e.g.
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
These function take a function as argument and return the return value of that function. atomicModifyIORef
is similar to that. It takes a function as an argument, and the intention is to return the return value of that function. There is just one complication: the argument function, has also to return a new value to be stored in the IORef
. Because of that, atomicModifyIORef
requires from that function to return two values. Of course, this case is not completely similar with the bracket case (e.g. there is no IO
involved, we are not dealing with exception safety, etc), but this analogy gives you an idea.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With