Stm and commit — Building Transactions
Stm<A, E> describes a transactional computation. It is lazy and pure with respect to the outside world: it can read/write transactional cells, fail with E, or retry.
commit: Lift Stm into Effect
use effectful::{Effect, Stm, TRef, commit, run_blocking};
let ref_a: TRef<i32> = run_blocking(commit(TRef::make(1)), ())?;
let ref_b: TRef<i32> = run_blocking(commit(TRef::make(2)), ())?;
let transaction: Stm<i32, ()> = ref_a.read_stm().flat_map(move |a| {
ref_b.read_stm().map(move |b| a + b)
});
let effect: Effect<i32, (), ()> = commit(transaction);
let result = run_blocking(effect, ())?;
commit(stm) returns Effect<A, E, R>. The transaction is executed when the effect runs. On commit conflicts or Stm::retry(), it retries.
atomically
atomically(stm) is an alias for commit(stm). It still returns an Effect; run it with run_blocking, run_async, or compose it with other effects.
use effectful::{atomically, run_blocking};
let effect = atomically(counter.modify_stm(|n| (n + 1, n + 1)));
let value = run_blocking(effect, ())?;
Stm::fail
Transactions can fail with typed errors:
use effectful::{Stm, TRef};
fn withdraw(account: TRef<u64>, amount: u64) -> Stm<u64, InsufficientFunds> {
account.read_stm().flat_map(move |balance| {
if balance < amount {
Stm::fail(InsufficientFunds)
} else {
account
.write_stm(balance - amount)
.map(move |_| balance - amount)
}
})
}
Stm::fail(e) aborts the current transaction immediately. commit propagates that E through the returned effect.
Stm::retry
Sometimes a transaction should wait until a condition is true rather than fail:
use effectful::{Stm, TRef};
fn dequeue(queue: TRef<Vec<Item>>) -> Stm<Item, ()> {
queue.read_stm().flat_map(move |items| {
if items.is_empty() {
Stm::retry()
} else {
let item = items[0].clone();
queue.write_stm(items[1..].to_vec()).map(move |_| item)
}
})
}
Stm::retry() asks the commit loop to restart later. TQueue::take uses this behavior to wait while empty.
Composing Transactions
Transactions compose with flat_map and map.
let big_transaction: Stm<(), AppError> = transfer_funds(from, to, amount)
.flat_map(move |_| record_audit_log(from, to, amount));
let effect = commit(big_transaction);
The composed transaction commits as a unit. If any part fails, retries, or observes a conflict, the whole transaction does not partially commit.