Can we modify the execute around idiom to use a mutable self?

Viewed 91

Short summary

I'm unable to find a way to wrap diesels transaction API in my own API, due to diesels use of the "execute around" idiom for transaction handling coupled with its non-&mut self arguments. This means I currently have to make some functions which really should mutate state also non-&mut self and use interior mutability. This feels ugly and I'm wondering if there is a workaround.

Long version

The rust diesel library uses a technique like the "execute around" idiom for handling transactions. For example, to use it to insert two names into a database within a transaction would look like this.

pub fn perform_insert_two_names_transactionally<C:Connection>(
    conn: &C,
    name1: &str,
    name2: &str) {

    conn.transaction::<_, Error, _>(|| {

        diesel::insert_into(users)
            .values(name.eq(name1))
            .execute(&conn)?;

        diesel::insert_into(users)
            .values(name.eq(name2))
            .execute(&conn)?;

        Ok(())
    })?;
}

The signature for the transaction function is

fn transaction<T, E, F>(&self, f: F) -> Result<T, E>
where
    F: FnOnce() -> Result<T, E>,
    E: From<Error>,

We can create a simplified version of this so that we don't need to build with diesel and define tables etc. We're really only using it for type and borrow checking.

pub struct Connection {}

pub enum ConnectionError {}

impl Connection {
    pub fn add_user(&self, name: &str) -> Result<(), ConnectionError> {
        Ok(())
    }
    pub fn transaction<T, E, F>(&self, f: F) -> Result<T, E>
    where
        F: FnOnce() -> Result<T, E>,
    {
        self.begin_transaction();
        let result = f();
        if result.is_ok() {
            self.end_transaction();
        } else {
            self.abort_transaction();
        }
        return result;
    }
    fn begin_transaction(&self) {}
    fn end_transaction(&self) {}
    fn abort_transaction(&self) {}
}

pub fn perform_insert_two_names_transactionally(
    conn: &Connection,
    name1: &str,
    name2: &str,
) -> Result<(), ConnectionError> {
    conn.transaction(|| {
        conn.add_user(name1)?;
        conn.add_user(name2)?;
        Ok(())
    })?;

    Ok(())
}

The key thing to note is that the add_user, and transaction functions do not take &mut self, just &self and this feels like a lie. In my version, I'd like them to take &mut self so that it is more clear that they are changing the application state.

If we try changing the usage of &self to &mut self (see this code) we get the following error:

error[E0501]: cannot borrow `*conn` as mutable because previous closure requires unique access
  --> src/lib.rs:32:5
   |
32 |       conn.transaction(|| {
   |       ^    ----------- -- closure construction occurs here
   |       |    |
   |  _____|    first borrow later used by call
   | |
33 | |         conn.add_user(name1)?;
   | |         ---- first borrow occurs due to use of `conn` in closure
34 | |         conn.add_user(name2)?;
35 | |         Ok(())
36 | |     })?;
   | |______^ second borrow occurs here

We can get around this constraint by changing the signature of the function passed to transaction to accept a &mut Connection, which can then be used to perform the mutable calls.

pub struct Connection {}

pub enum ConnectionError {}

impl Connection {
    pub fn add_user(&mut self, name: &str) -> Result<(), ConnectionError> {
        Ok(())
    }
    pub fn transaction<T, E, F>(&mut self, f: F) -> Result<T, E>
    where
        F: FnOnce(&mut Connection) -> Result<T, E>,
    {
        self.begin_transaction();
        let result = f(self);
        if result.is_ok() {
            self.end_transaction();
        } else {
            self.abort_transaction();
        }
        return result;
    }
    fn begin_transaction(&mut self) {}
    fn end_transaction(&mut self) {}
    fn abort_transaction(&mut self) {}
}

pub fn perform_insert_two_names_transactionally(
    conn: &mut Connection,
    name1: &str,
    name2: &str,
) -> Result<(), ConnectionError> {
    conn.transaction(|conn| {
        conn.add_user(name1)?;
        conn.add_user(name2)?;
        Ok(())
    })?;

    Ok(())
}

The core problem is that when wrapping the diesel transactions we don't have access to begin_transaction, end_transaction and abort_transaction -- we need to use the diesel::Connection::transaction function instead.

The simplified version of this is

pub struct DieselConnection {}
pub struct WrapperConnection {
    pub conn:DieselConnection
}

pub enum ConnectionError {}

impl DieselConnection {
    pub fn add_user(&self, name: &str) -> Result<(), ConnectionError> {
        Ok(())
    }
    pub fn transaction<T, E, F>(&self, f: F) -> Result<T, E>
    where
        F: FnOnce() -> Result<T, E>,
    {
        self.begin_transaction();
        let result = f();
        if result.is_ok() {
            self.end_transaction();
        } else {
            self.abort_transaction();
        }
        return result;
    }
    fn begin_transaction(&self) {}
    fn end_transaction(&self) {}
    fn abort_transaction(&self) {}
}

impl WrapperConnection {
    pub fn add_user(&mut self, name: &str) -> Result<(), ConnectionError> {
        self.conn.add_user(name)
    }
    pub fn transaction<T, E, F>(&mut self, f: F) -> Result<T, E>
    where
        F: FnOnce(&mut WrapperConnection) -> Result<T, E>,
    {
        self.conn.transaction( || { f(self) } )
    }
}

pub fn perform_insert_two_names_transactionally(
    conn: &mut WrapperConnection,
    name1: &str,
    name2: &str,
) -> Result<(), ConnectionError> {
    conn.transaction(|conn| {
        conn.add_user(name1)?;
        conn.add_user(name2)?;
        Ok(())
    })
}

also available on the playground.

However, this gives the following error:

error[E0500]: closure requires unique access to `self` but it is already borrowed
  --> src/lib.rs:38:32
   |
38 |         self.conn.transaction( || { f(self) } )
   |         --------- -----------  ^^     ---- second borrow occurs due to use of `self` in closure
   |         |         |            |
   |         |         |            closure construction occurs here
   |         |         first borrow later used by call
   |         borrow occurs here

error: aborting due to previous error; 1 warning emitted

again, this makes sense.

I don't think this can be worked around by placing the connection in a RefCell or similar - but I'd love to be told I'm wrong.

At the moment I've just given up on wrapping the API in a way that matches my mutability expectations - my functions are all pretty much all using &self and work around the need for mutability using interior mutation using RefCell on their own data.

Is there some way wrap an API like the diesel transaction API and get the mutability types that I want?

1 Answers

I managed to work around this by noting that if we could clone the wrapped connection we could write the transaction wrapper as

impl WrapperConnection {
    pub fn transaction<T, E, F>(&mut self, f: F) -> Result<T, E>
    where
        F: FnOnce(&mut WrapperConnection) -> Result<T, E>,
    {
        let conn_copy = self.conn.clone();
        conn_copy.transaction( || { f(self) } )
    }
}

(which compiles if you add #[derive(Clone)] to DieselConnection)

But in reality, the base connection doesn't implement Clone. So instead of storing a DieselConnection in the WrapperConnection we can store a Rc<DieselConnection>, which does implement Clone.

The full working code is:

use std::rc::Rc;

pub struct DieselConnection {}

pub struct WrapperConnection {
    pub conn: Rc<DieselConnection>,
}

pub enum ConnectionError {}

impl DieselConnection {
    pub fn add_user(&self, name: &str) -> Result<(), ConnectionError> {
        Ok(())
    }
    pub fn transaction<T, E, F>(&self, f: F) -> Result<T, E>
    where
        F: FnOnce() -> Result<T, E>,
    {
        self.begin_transaction();
        let result = f();
        if result.is_ok() {
            self.end_transaction();
        } else {
            self.abort_transaction();
        }
        return result;
    }
    fn begin_transaction(&self) {}
    fn end_transaction(&self) {}
    fn abort_transaction(&self) {}
}

impl WrapperConnection {
    pub fn add_user(&mut self, name: &str) -> Result<(), ConnectionError> {
        self.conn.add_user(name)
    }
    pub fn transaction<T, E, F>(&mut self, f: F) -> Result<T, E>
    where
        F: FnOnce(&mut WrapperConnection) -> Result<T, E>,
    {
        let temp_conn = self.conn.clone();
        temp_conn.transaction(|| f(self))
    }
}

pub fn perform_insert_two_names_transactionally(
    conn: &mut WrapperConnection,
    name1: &str,
    name2: &str,
) -> Result<(), ConnectionError> {
    conn.transaction(|conn| {
        conn.add_user(name1)?;
        conn.add_user(name2)?;
        Ok(())
    })
}

which can be found in the rust playground here.

Related