Plebshot
Plebshot

Reputation: 220

Best practice to avoid writing duplicate code for sync traits and their async counterpart

I stumbled upon this issue while writing a small custom protocol that extends both the std::io::Write and futures::io::AsyncWrite (as well as the Read traits). I noticed writing a lot of duplicate code, as the protocol behaves exactly the same whether it's async or not. This is especially bad within the exhaustive tests, where i use both versions of Cursor and need to test both variants working together.

Is there a way to bridge both kinds of traits? Maybe a macro that generates both variants by just leaving out the .await and async parts (if applicable).

Reference code implementation

This is a bit numbed down.

impl<W> ProtoWriteExt for W
where
    W: Write,
{
    fn proto_write<T>(&mut self, value: &T) -> Result<(), ProtocolError>
    where
        T: Serialize,
    {
        // lots of code...

        // Only these calls change
        self.write_all(&bytes)?;

        // ...
    }
}

#[async_trait]
impl<W> ProtoAsyncWriteExt for W
where
    W: AsyncWrite + Unpin + Send + Sync,
{
    async fn proto_write<T>(&mut self, value: &T) -> Result<(), ProtocolError>
    where
        T: Serialize + Sync,
    {
        // Same code as above...

        // Only these calls change
        self.write_all(&bytes).await?;

        // ...
    }
}

Reference tests

There are gonna be a lot more tests like this and i would have to test the nonblocking version against the blocking one as well.


/// Writing a primitive value and then reading it results in an unchanged value.
#[test]
fn transfers_primitive_correctly() -> Result<(), ProtocolError> {
    let expected = 42;

    let mut cursor = std::io::Cursor::new(Vec::<u8>::new());
    cursor.proto_write(&expected)?;
    cursor.set_position(0);
    let result: i32 = cursor.proto_read()?;

    assert_eq!(expected, result);

    Ok(())
}

/// Writing a primitive value and then reading it results in an unchanged value.
#[tokio::test]
async fn async_transfers_primitive_correctly() -> Result<(), ProtocolError> {
    let expected = 42;

    let mut cursor = futures::io::Cursor::new(Vec::<u8>::new());
    cursor.proto_write(&expected).await?;
    cursor.set_position(0);
    let result: i32 = cursor.proto_read().await?;

    assert_eq!(expected, result);

    Ok(())
}

Upvotes: 1

Views: 423

Answers (2)

Emoun
Emoun

Reputation: 2507

The duplicate crate can create duplicates of code with targeted substitutions in it.

This could be especially useful in your tests, which could look like this:

use duplicate::duplicate_item;

#[duplicate_item(
    test_attr     name                                  async   add_await(code);
    [test]        [transfers_primitive_correctly]       []      [code];
    [tokio::test] [async_transfers_primitive_correctly] [async] [code.await];
)]
#[test_attr]
async fn name() -> Result<(), ProtocolError> {
    let expected = 42;

    let mut cursor = std::io::Cursor::new(Vec::<u8>::new());
    add_await([cursor.proto_write(&expected)])?;
    cursor.set_position(0);
    let result: i32 = add_await([cursor.proto_read()])?;

    assert_eq!(expected, result);

    Ok(())
}

This should expand to the test code you have.

I'm hesitant to advise you to use duplicate for your impl's as it seems to me that they are too different to be a good case for duplicate. I would first explore the possibility of refactoring the code, such that they simply call the same function. However, if that is not possible, duplicate could still be used.

Upvotes: 1

Chayim Friedman
Chayim Friedman

Reputation: 70940

The reqwest crate does all heavy lifting in async code, and within the blocking methods just blocks on the async futures. So do mongodb and postgres (based on the tokio-postgres crate).

There is a keyword generics initiative that will allow you to be generic over asyncness, but it is only early stages (see also the lang team meeting document).

You probably can do that by a macro, but it is pretty hard: the sync version will have to call sync methods while the async version will need to call async methods somehow. I believe this is possible, however I don't know a crate that provides that.

Upvotes: 3

Related Questions