What is the purpose of the extra result parameter of atomicModifyIORef?

426 Views Asked by At

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?

4

There are 4 best solutions below

0
dfeuer On BEST ANSWER

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.

9
redneb On

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.

1
safsaf32 On

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.

0
Petr On

The way I like to view this is via the State monad. A stateful operation modifies some internal state and additionally yields an output. Here the state is inside an IORef and the result is returned as part of the IO operation. So we can reformulate the function using State as follows:

import Control.Monad.State
import Data.IORef
import Data.Tuple (swap)

-- | Applies a stateful operation to a reference and returns its result.
atomicModifyIORefState :: IORef s -> State s a -> IO a
atomicModifyIORefState ref state = atomicModifyIORef ref (swap . runState state)