3 ms·
You don’t need anything along the lines of typestate, though it can fit in fine as well: having all your functions consuming self and returning a new Self (exce
by chrismorgan 3y ago
You don’t need anything along the lines of typestate, though it can fit in fine as well: having all your functions consuming self and returning a new Self (except the consuming “destructor”) is sufficient. It’s just an ergonomics pain since you need to assign it to something every time:
#[must_use]
struct Transaction;
impl Transaction {
fn new() -> Self { … }
fn action(self, …) -> Self { … }
fn commit(self) -> Result<(), …> { … }
}
let mut tx = Transaction::new();
tx = tx.action(…); // ← `tx =` is the difference.
// (Any panic at this point will roll back the transaction via Drop,
// but any errors in the process will be swallowed or abort or who knows what, there’s no standard.)
tx.commit()?; // unused-must-use warning on the last assignment of tx (`tx = tx.action(…);`) if this line is absent
A hypothetical #[must_consume] would let it be like this:
#[must_consume]
struct Transaction;
impl Transaction {
fn new() -> Self { … }
fn action(&mut self, …) { … }
fn commit(self) -> Result<(), …> { …; drop(self); Ok(()) }
}
let mut tx = Transaction::new();
tx.action(…);
// (Same panic behaviour.)
tx.commit()?; // unused-must-consume warning on the last assignment of tx (`let mut tx = Transaction::new();`) if this line is absent
With putting the state into the type, you’d be limited to annotating your entire generic type with #[must_use], not individual states. (#[must_use] won’t propagate through generic parameters, nor should it.)
- weinzierl 3y agoThat makes it clearer. Thanks!