Skip to content

Latest commit

 

History

History
100 lines (71 loc) · 4.05 KB

README.md

File metadata and controls

100 lines (71 loc) · 4.05 KB

async-generic

Write code that can be both async and synchronous without duplicating it.

Why solve this problem now?

This macro set is intended as an interim solution for the problem space that will eventually be covered by the Rust Keyword Generic Initiative.

As of this writing (September 2023), the official status of that project is listed as still in the "Experimental" stage, so deployment in the language is still likely many months away if not longer.

So ... what do we do now if we need async-generic code? We build our own, using proc macros. And that's exactly what this crate is.

I'll happily mark this crate as deprecated when keyword generics land officially in the language. Until then, hopefully it solves some problems for you, too!

IMPORTANT: This crate is quite simple, so I expect there will be few releases beyond the 1.0.0 release. If you encounter issues, pelase do file them here; I use this crate routinely in other projects and will be watching, even if I don't update it regularly.

User's guide

The async_generic crate introduces a single proc macro also named async_generic which can be applied as an attribute to any function (either inside a struct or not).

The macro outputs two versions of the function, one synchronous and one that's async. The functions are identical to each other, except as follows:

  • When writing the async flavor of the function, the macro inserts the async modifier for you and renames the function (to avoid a name collision) by adding an _async suffix to the existing function name.
  • The attribute macro may contain an async_signature argument. If that exists, the async function's argument parameters are replaced. (See example below.)
  • You can write if _sync or if _async blocks inside this block. The contents of these blocks will only appear in the corresponding sync or async flavors of the functions. You may specify an else clause, which will only appear in the opposite flavor of the function. You may not combine _sync or _async with any other expression. (These aren't really variables in the function scope, and they will cause "undefined identifier" errors if you try that.)

A simple example:

use async_generic::async_generic;

#[async_generic]
fn do_stuff() -> String {
    // Also: async fn do_stuff_async() -> String {...}
    if _async {
        my_async_stuff().await
    } else {
        "not async".to_owned()
    }
}

async fn my_async_stuff() -> String {
    "async".to_owned()
}

#[async_std::main]
async fn main() {
    println!("sync => {}", do_stuff());
    println!("async => {}", do_stuff_async().await);
}

An example with different function arguments in the sync and async flavors:

use async_generic::async_generic;

#[async_generic(async_signature(thing: &AsyncThing))]
fn do_stuff(thing: &SyncThing) -> String {
    // Also: async fn do_stuff_async(thing: &AsyncThing) -> String {...}
    if _async {
        thing.do_stuff().await
    } else {
        thing.do_stuff()
    }
}

struct SyncThing {}

impl SyncThing {
    fn do_stuff(&self) -> String {
        "sync".to_owned()
    }
}

struct AsyncThing {}

impl AsyncThing {
    async fn do_stuff(&self) -> String {
        "async".to_owned()
    }
}

#[async_std::main]
async fn main() {
    let st = SyncThing {};
    let at = AsyncThing {};

    println!("sync => {}", do_stuff(&st));
    println!("async => {}", do_stuff_async(&at).await);
}

Why not use maybe-async?

This crate is loosely derived from the excellent work of the maybe-async crate, but is intended to solve a subtly different problem.

Use maybe-async when you know at compile-time whether each crate in your dependency tree will be used in an async or synchronous fashion. In that model, you can't have both at once.

Use async-generic when you wish to have both async and synchronous versions of an API at the same time and want to reuse most of the implementation.