Compiler forced transactions


#1

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.


#2

How about taking ownership over MyContainer and returning it back in commit()?


#3

Hi Vitaly, thanks for the reply!

How about taking ownership over MyContainer and returning it back in commit()?

MyContainer is also inside another container which I can only access as a mutable reference. It’s turtles all the way down. From my exprience I can either use mutable references or I could take ownership. If I combine the two then the location where the stitch happens will probably contain something like an Option<> or a mem::replace. Both give me hard time sleeping at night.

I had another idea: using a closure. This is what it looks like:

struct MyContainer {
    ms: MyState,
}

struct MyState {
    num: u32,
}

struct Adder<'a> {
    ms: &'a MyState,
    added: u32,
}

impl<'a> Adder<'a> {
    fn new(ms: &MyState) -> Adder {
        Adder {
            ms,
            added: 0,
        }
    }
    fn inc(&mut self) {
        self.added += 1;
    }
}

impl MyState {
    fn adder_transact<F>(&mut self, f: F) 
    where F: FnOnce(&mut Adder) {
        self.num += {
            let mut adder = Adder::new(self);
            f(&mut adder);
            adder.added
        };
    }
}

fn add_three(mc: &mut MyContainer) {
    mc.ms.adder_transact(|adder| {
        adder.inc();
        adder.inc();
        adder.inc();
    });
}

fn main() {
    let mut mc = MyContainer {
        ms: MyState {num: 5},
    };
    add_three(&mut mc);
    assert_eq!(mc.ms.num, 8);
}

I am still wondering if I’m doing it the Rust way, or am I missing something.


#4

A closure could work well if the scoping of the transaction works out (i.e. the scope of the transaction isn’t scattered around a bunch of different functions).

As for a Drop impl on Adder, I agree that it’s a bit odd. Not so much because you need a drop(adder) in the middle of a function (you could, e.g., put the adder into a {...} scope), but more because of inverted semantics - you typically want an explicit commit(), and an implicit action would be to roll back the transaction. Here it would be backwards.

If you can’t take ownership of the values, then I can’t see anything better. It’d be nice if one could mandate that a certain method be called on a type before it drops, but that’s not available.


#5

This will be handled by the borrow checker anyway.

Another approach to transactions is to use a closure:

collection.with_transaction(|adder| {
    adder.inc();
    adder.inc();
});

#6

I thought that you can get at least a warning with #[must_use] attribute, but unfortunately, you’d need to restructure your code to be a method chain, as even assigning to a variable counts as usage. Playground


#7

Yes. This is not an accident, I designed the code that way!

I think that this is my current choice of how to implement it. I should have asked you people earlier, it took me a while to think about using a closure.

I tried it too. I wish that there was a way to mark some struct as “can not be implicitly dropped”.

@vitalyd: I have to agree.

Thank you all for the great ideas!
real.