10

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?

Robin Green
  • 32,079
  • 16
  • 104
  • 187
leftaroundabout
  • 117,950
  • 5
  • 174
  • 319

4 Answers4

12

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.

redneb
  • 21,794
  • 6
  • 42
  • 54
  • So, had it been `atomicModifyIORef :: IORef a -> (a -> a) -> IO a`, returning the old value, would have served the same purpose (and be simpler, IMO). Interesting. – chi Sep 22 '16 at 14:58
  • What I don't understand is, why would I need this feature for `atomicModifyIORef` but not for `modifyIORef`? – leftaroundabout Sep 22 '16 at 15:16
  • @leftaroundabout Well, `modifyIORef` doesn't provide any atomicity guarantees anyway, so it wouldn't be that useful for it. – redneb Sep 22 '16 at 15:22
  • @chi That would have been simpler, but the current solution can do more things. For example, you could implement some form of atomic compare-and-swap that returns a boolean flag that indicates if the swap did happen. – redneb Sep 22 '16 at 15:25
  • Well, instead of `atomicModifyIORef ref (\old -> if swap old then (new, True) else (old, False)` you could do `swap <$> simplerAtomicModifyIORef ref (\old -> if swap old then new else old)` -- the only downside is that you need to compute the pure predicate `swap old` twice. But this should be equivalent to your compare-and-swap (I think?) – chi Sep 22 '16 at 16:10
  • 1
    So the point is, without concurrency I might as well use `old <- readIORef r; writeIORef r $ f old` if I want to keep the old value, whereas in the multithreaded case I _must_ use `old <- atomicModify r (\x -> (f x, x))` to ensure the new value actually corresponds to `f` applied to the old value, of which I get a copy in `old`. Have I understood that correctly? – leftaroundabout Sep 22 '16 at 18:26
  • @leftaroundabout Exactly. – redneb Sep 22 '16 at 18:47
  • @leftaroundabout Did this post answer your question? – safsaf32 Sep 23 '16 at 22:30
  • @safsaf32: well, eventually yes, though to be honest I think the point isn't explained very incisively here – if you can phrase a better answer, I'll accept it. – leftaroundabout Sep 24 '16 at 14:21
2

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.

safsaf32
  • 1,517
  • 2
  • 13
  • 18
  • Interesting comparison. I'll nevertheless accept [dfeuer's answer](http://stackoverflow.com/a/39682119/745903) now since the “why is this needed for `atomicModify` but for `modify`” part was what this question was mainly about. – leftaroundabout Sep 25 '16 at 10:34
2

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.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
1

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)
Petr
  • 62,528
  • 13
  • 153
  • 317