In Haskell, a possible approach is to use software transactional memory for this.
-- silly example mixing IO and STM
doOperation :: TVar Int -> IO ()
doOperation x = do -- IO monad
putStrLn "Incrementing value!"
atomically $ do -- STM monad
v <- readTVar x
-- putStrLn "We can't do I/O here, in the middle of STM!"
-- print v
writeTVar x $! v+1
putStrLn "Incrementing complete."
Using atomically
, we can run STM actions in the middle of the IO actions. This allows us to read and write x
, so to increment it. Alternatively, modifyTVar' (+1)
would also do the increment more conveniently, but above I wanted to separate the reading from the writing.
By comparison, there is no way to run IO actions in the middle of the STM atomic transaction. If we try to uncomment the IO actions in the code above, the compiler raises a type error because IO
is not STM
. This is important, since we must not print v
in the middle of a transaction which could still be rolled back -- we can't undo the print v
, or in general other IO computations which might even use the disk or the network. (The GHC devs use launchMissiles
as the archetypal example of IO having international side effects, with no possible "undo".)
Operationally, the STM monad implementation keeps track of the changes to the variable x
. When reading, it logs the old value of x
(no locks at this time).
When writing, the new value is also logged, but not written (no locks).
Subsequent reads within the transaction read the new value of x
(no locks).
At the very end of the transaction, we enter the commit phase: we lock all the involved variables (using a deadlock-free lock ordering). We then test if they still contain the old values. If they don't, we rollback the transaction, and retry it again. If they do, we write the new values (from the log), unlock the variables, and we are done.
STM is not as efficient as custom algorithms for mutual exclusion / deadlock-avoidance which can be used for specific problems. It is however very general, and unlike basic locks, very composable and flexible.
Pure FP makes the approach safer by checking that the programmer only uses safe operations in the transactions (no IO).
haskell
andclojure
tags.