Forbidding calls to certain functions in a closure

I'm having a bit of a weird requirement for an API. What I want to do is forbid certain operations inside specific closures. For example:

transaction(|| {
  operation_1(); // safe
  operation_2(); // Can't be rolled back and should not compile!!!
  operation_3(); // safe
});

Is there a way of modelling this with Rust's type system? It reminds me a bit of unsafe, but in a more generalised way, like not safe just inside of the closure.

The only thing I could think of is to abuse some of the existing auto traits, like Send or Sync, and force users to write:

transaction(|ctx: &Ctx| {
  ctx.operation_1(); // safe
  ctx.operation_2(); // Can't be rolled back and should not compile!!!
  ctx.operation_3(); // safe
});

But this also means that operations that are outside the transaction will always need to use a context and is just overcomplicating things. I would rather fail at runtime in this case and panic on operation_2. But being able to fail at compile time would just be so neat.

You definitely shouldn't be abusing auto traits for this – people will (rightfully) be furious if their otherwise perfectly Send + Sync code doesn't work with your API — or worse yet, it incorrectly compiles and causes UB due to a data race.

As far as I see, this has nothing to do with the functions being called "in a closure". The important thing is that you want to disallow operations in a transaction. So that's what you should model with a trait: transactional operations.

Then, you should express using the type system that a combination of transactional operations only is still transactional, and even a single non-transactional operation makes the whole chain non-transactional. That's essentially an "all" or "logical and" operation. The standard trick for expressing such reductions/aggregations at the type level is recursing based on 2-tuples. So a pair of transactionals is transactional (where either the head or the tail may itself be a tuple, etc.)

All in all, something like this seems to express what you want, and its essence is the following:


trait Operation {
    type Output;

    fn execute(self) -> Result<Self::Output, Error>;
}

impl<T: Operation> Operation for (T,) {
    type Output = <T as Operation>::Output;

    fn execute(self) -> Result<Self::Output, Error> {
        self.0.execute()
    }
}

impl<H: Operation, T: Operation> Operation for (H, T) {
    type Output = (H::Output, T::Output);

    fn execute(self) -> Result<Self::Output, Error> {
        let head = self.0.execute()?;
        let tail = self.1.execute()?;
        Ok((head, tail))
    }
}

/// Marks the subset of operations that must be transactional
trait Transactional: Operation {}

impl<T: Transactional> Transactional for (T,) {}
impl<H: Transactional, T: Transactional> Transactional for (H, T) {}
8 Likes

Thanks so much for this! It's a much better way to think about it than anything I have been coming up with.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.