What is the purpose of the extra result parameter of atomicModifyIORef?
Asked Answered
B

4

11

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?

Buie answered 22/9, 2016 at 13:25 Comment(0)
D
3

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.

Dickson answered 25/9, 2016 at 0:3 Comment(0)
Y
13

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.

Yoko answered 22/9, 2016 at 13:36 Comment(9)
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.Flophouse
What I don't understand is, why would I need this feature for atomicModifyIORef but not for modifyIORef?Buie
@Buie Well, modifyIORef doesn't provide any atomicity guarantees anyway, so it wouldn't be that useful for it.Yoko
@Flophouse 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.Yoko
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?)Flophouse
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?Buie
@Buie Exactly.Yoko
@Buie Did this post answer your question?Encumbrancer
@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.Buie
E
3

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.

Encumbrancer answered 24/9, 2016 at 20:54 Comment(1)
Interesting comparison. I'll nevertheless accept dfeuer's answer now since the “why is this needed for atomicModify but for modify” part was what this question was mainly about.Buie
D
3

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.

Dickson answered 25/9, 2016 at 0:3 Comment(0)
G
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)
Gilemette answered 20/1, 2018 at 13:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.