I want to write code that enforces transactional behaviour of a Struct at compile time, but I am having problems implementing it.
A simplified code example:
struct MyContainer {
ms: MyState,
}
struct MyState {
num: u32,
}
struct Adder<'a> {
ms: &'a mut MyState,
added: u32,
}
impl<'a> Adder<'a> {
fn new(ms: &mut MyState) -> Adder {
Adder {
ms,
added: 0,
}
}
fn inc(&mut self) {
self.added += 1;
}
fn commit(self) {
self.ms.num += self.added;
}
}
fn add_three(mc: &mut MyContainer) {
let mut adder = Adder::new(&mut mc.ms);
adder.inc();
adder.inc();
// mc.ms.num -= 1; // Can not do this here!
// as we are in a middle of a transaction.
adder.inc();
adder.commit(); // What if we forget this line?
}
fn main() {
let mut mc = MyContainer {
ms: MyState {num: 5},
};
add_three(&mut mc);
assert_eq!(mc.ms.num, 8);
}
The important struct in this code is MyState
. It is contained inside MyContainer
and can not be moved out of it. MyState
contains some state that should only be mutated in a transactional way. (In the example the only mutations are additions by 1, but in my real code transactional stuff happen instead)
In this example, I implement a transaction by wrapping MyState
with the Adder
struct. Adder is an addition transaction. Once MyStruct
was wrapped by Adder
, the only thing can be done is calling Adder::inc()
. After a few calls to Adder::inc()
one can call Adder::commit()
to finish the Adder transaction and keep working with MyState
.
Take a look at the function add_three()
. It demonstrates what I'm trying to enforce in compile time.
During an Adder
transaction it is not possible to mutate MyState
, because it is borrowed. Only after the transaction is commited using commit()
it is again possible to acess MyState
.
My problem with this scheme is that the user of MyStruct
and Adder
might forget to use Adder::commit()
. Adder
will be dropped, and no mutation will be applied to MyState
.
I want to know if there is a way to make sure (during compilation) that commit()
is always called inside the scope. In other words, is there a way to make Adder non droppable, so that the only way to get rid of him would be to use commit()
? If there is a way to do this, this will solve my problem.
Another possible solution I thought of was to implement Drop for Adder, and have do the same job done in commit. However, I think that it is somewhat strange to call drop(adder) in the middle of a function to commit a transaction. In addition, the signature of drop is fn drop(&mut self)
, while I would like to have the signature fn commit(self)
, talking ownership of Adder
.
A different possibility would be to have Adder::new
take ownership over MyState, but in that case I will need to have:
struct MyContainer {
opt_ms: Option<MyState>,
}
This will force me to unwrap() every time I'm trying to read MyState
from MyContainer
.
Any ideas are appreciated!
real.