Haskell STM alwaysSucceeds
Asked Answered
P

1

11

There is a function in haskell's stm library with the following type signature:

alwaysSucceeds :: STM a -> STM ()

From what I understand of STM in haskell, there are three ways that something can "go wrong" (using that term loosely) while an STM computation is executing:

  1. The value of an TVar that has been read is changed by another thread.
  2. An user-specified invariant is violated. This seems to usually be triggered by calling retry to make it start over. This effectively makes the thread block and then retry once a TVar in the read set changes.
  3. An exception is thrown. Calling throwSTM causes this. This one differs from the first two because the transaction doesn't get restarted. Instead, the error is propagated and either crashes the program or is caught in the IO monad.

If these are accurate (and if they are not, please tell me), I can't understand what alwaysSucceeds could possibly do. The always function, which appears to be built on top of it, seems like it could be written without alwaysSucceeds as:

--This is probably wrong
always :: STM Bool -> STM ()
always stmBool = stmBool >>= check

The documentation for alwaysSucceeds says:

alwaysSucceeds adds a new invariant that must be true when passed to alwaysSucceeds, at the end of the current transaction, and at the end of every subsequent transaction. If it fails at any of those points then the transaction violating it is aborted and the exception raised by the invariant is propagated.

But since the argument is of type STM a (polymorphic in a), it can't use the value that the transaction returns for any part of the decision making. So, it seems like it would be looking for the different types of failures that I listed earlier. But what's the point of that? The STM monad already handles the failures. How would wrapping it in this function affect it? And why does the variable of type a get dropped, resulting in STM ()?

Phantasm answered 6/9, 2014 at 22:28 Comment(1)
Since your comment there reminded me: there's a current GHC proposal to deprecate this feature.Capon
L
8

The special effect of alwaysSucceeds is not how it checks for failure at the spot it's run (running the "invariant" action alone should do the same thing), but how it reruns the invariant check at the end of transactions.

Basically, this function creates a user-specified invariant as in (2) above, that has to hold not just right now, but also at the end of later transactions.

Note that a "transaction" doesn't refer to every single subaction in the STM monad, but to a combined action that is passed to atomically.

I guess the a is dropped just for convenience so you don't have to convert an action to STM () (e.g. with void) before passing it to alwaysSucceeds. The return value will be useless anyhow for the later repeated checks.

Lautrec answered 7/9, 2014 at 5:17 Comment(4)
Got it. The real world haskell tutorial chapter 28 gives a fairly good example of this. So it seems like, if I understand correctly, alwaysSucceeds and always should both be called on something that has a possibility of calling throwSTM. It also seems like using a lot of these would significantly impact performance since they are rerun after every transaction.Phantasm
@AndrewThaddeusMartin After some further testing, I don't think it has to be throwSTM, it seems like it can be retry based (including check), although if your invariant then fails, your transaction will retry indefinitely unless you have something in another thread that causes it to eventually succeed.Capon
@AndrewThaddeusMartin An instructive test is: do t <- newTVarIO 0; atomically . alwaysSucceeds $ readTVar t >>= check . (>= 0); forkIO $ forever (do threadDelay 10000000; atomically $ modifyTVar t (max 1)); replicateM_ 3 . atomically . modifyTVar t $ subtract 1Capon
I ran the test. Clever of you, it finally succeeds after 30 seconds, which makes sense. Although I feel like this way of using always seems a little odd and makes reasoning more difficult. Also, you could write functions like f = alwaysSucceeds . alwaysSucceeds . alwaysSucceeds which seems kind of bizarre. It feels like the result being in the IO monad instead would make more sense for alwaysSucceeds.Phantasm

© 2022 - 2024 — McMap. All rights reserved.