Summary
Currently, STMEngine.Atomic<T> executes a single, flat transaction defined as a lambda. There is no support for composing smaller transactional operations into larger atomic units, nor for expressing alternative retry paths. This limits reusability: if two transactional operations already exist as separate functions, combining them into a single atomic commit requires inlining their logic into one lambda.
Motivation
Today, atomically incrementing one STMVariable<int> and decrementing another requires writing all the logic in a single closure:
await STMEngine.Atomic<int>(tx =>
{
var a = tx.Read(varA);
var b = tx.Read(varB);
tx.Write(varA, a + 1);
tx.Write(varB, b - 1);
});
If Increment(varA) and Decrement(varB) are already defined as reusable transactional building blocks, there is no way to combine them into a single atomic transaction without duplicating their internals.
With composable transactions, the goal would be to express this as sequential composition of existing operations, both running inside the same Transaction<T> and committed together.
Similarly, an OrElse combinator would enable fallback strategies: attempt a transactional read-modify-write on varA; if that path hits a conflict and would retry, automatically fall back to the same operation on varB, all within the same transaction boundary, without the caller needing manual retry orchestration.
Proposed design
Introduce an StmAction<T> abstraction representing a composable transactional operation, with two combinators:
AndThen: sequential composition: both actions run in the same Transaction<T> context. If either causes a conflict at commit, the entire composed transaction retries via STMEngine.
OrElse: alternative composition: if the primary action triggers a retry (conflict), the transaction state is rolled back to a checkpoint taken before the primary action, and the fallback is attempted. If both paths fail, the whole transaction retries.
A new STMEngine.Atomic<T>(StmAction<T>, ...) overload would interpret the composed action tree.
Design considerations
| Aspect |
Notes |
| AndThen semantics |
Both actions share the same Transaction<T> instance. same _reads, _writes, _snapshotVersions. Conflict in either causes a full retry. |
| OrElse semantics |
Requires checkpoint/rollback support in Transaction<T>. snapshotting _reads, _writes, and _snapshotVersions at OrElse boundaries and restoring them on fallback. This is the most significant internal change. |
| Checkpoint/rollback |
Transaction<T>.Clear() already exists; a new Checkpoint()/Restore() pair would save and restore the three dictionaries. Dictionary copy cost is proportional to the read/write set size, which is typically small. |
| Nested transactions |
True nesting (child commits independently) is not proposed. Composition is always flattened into a single commit, consistent with the Haskell STM model. |
| Return values |
Consider a variant with a result type for actions that produce a value, enabling map/bind-style composition. This would complement the existing Atomic<T, TResult> overloads. |
| Backward compatibility |
Fully additive, existing lambda-based Action<ITransaction<T>> and Func<ITransaction<T>, Task> overloads remain unchanged. |
Implementation outline
- Add the
StmAction<T> base abstraction with AndThen and OrElse combinators, plus concrete implementations (SequenceAction<T>, OrElseAction<T>, LambdaAction<T>).
- Add
Checkpoint() and Restore() methods to Transaction<T> for saving and rolling back _reads, _writes, and _snapshotVersions.
- Add
STMEngine.Atomic<T>(StmAction<T>, ...) overload that walks the action tree and executes it against a Transaction<T>.
- Unit tests, compose read/write operations on multiple
STMVariable<int> instances, verify atomicity of AndThen, verify fallback semantics of OrElse under contention.
- Update README with composition examples using the STMSharp API.
Summary
Currently,
STMEngine.Atomic<T>executes a single, flat transaction defined as a lambda. There is no support for composing smaller transactional operations into larger atomic units, nor for expressing alternative retry paths. This limits reusability: if two transactional operations already exist as separate functions, combining them into a single atomic commit requires inlining their logic into one lambda.Motivation
Today, atomically incrementing one
STMVariable<int>and decrementing another requires writing all the logic in a single closure:If
Increment(varA)andDecrement(varB)are already defined as reusable transactional building blocks, there is no way to combine them into a single atomic transaction without duplicating their internals.With composable transactions, the goal would be to express this as sequential composition of existing operations, both running inside the same
Transaction<T>and committed together.Similarly, an
OrElsecombinator would enable fallback strategies: attempt a transactional read-modify-write onvarA; if that path hits a conflict and would retry, automatically fall back to the same operation onvarB, all within the same transaction boundary, without the caller needing manual retry orchestration.Proposed design
Introduce an
StmAction<T>abstraction representing a composable transactional operation, with two combinators:AndThen: sequential composition: both actions run in the sameTransaction<T>context. If either causes a conflict at commit, the entire composed transaction retries viaSTMEngine.OrElse: alternative composition: if the primary action triggers a retry (conflict), the transaction state is rolled back to a checkpoint taken before the primary action, and the fallback is attempted. If both paths fail, the whole transaction retries.A new
STMEngine.Atomic<T>(StmAction<T>, ...)overload would interpret the composed action tree.Design considerations
Transaction<T>instance. same_reads,_writes,_snapshotVersions. Conflict in either causes a full retry.Transaction<T>. snapshotting_reads,_writes, and_snapshotVersionsat OrElse boundaries and restoring them on fallback. This is the most significant internal change.Transaction<T>.Clear()already exists; a newCheckpoint()/Restore()pair would save and restore the three dictionaries. Dictionary copy cost is proportional to the read/write set size, which is typically small.Atomic<T, TResult>overloads.Action<ITransaction<T>>andFunc<ITransaction<T>, Task>overloads remain unchanged.Implementation outline
StmAction<T>base abstraction withAndThenandOrElsecombinators, plus concrete implementations (SequenceAction<T>,OrElseAction<T>,LambdaAction<T>).Checkpoint()andRestore()methods toTransaction<T>for saving and rolling back_reads,_writes, and_snapshotVersions.STMEngine.Atomic<T>(StmAction<T>, ...)overload that walks the action tree and executes it against aTransaction<T>.STMVariable<int>instances, verify atomicity ofAndThen, verify fallback semantics ofOrElseunder contention.