Soft question: async trait fn

  1. I suddenly find myself accidentally writing an async trait fn.

  2. Is async in Traits - Asynchronous Programming in Rust still state-of-the-art regarding async trait fns? I.e. (1) they are not supported, (2) use the async-trait crate, and (2) pay a heap alloc per call.

  3. Is async trait fn a code smell ?

  4. What solution do people generally use? (1) try to reorganize code around to not use async trait fn, (2) use that crate, or (3) manually return futures ?

1 Like

Yes. Or you could pretend you're David Tolnay and re-write the crate on your own. Either way, same result - you incur a Box.

No. Just inconvenient at the moment.

All three are valid IMO. (1) is okay since you don't always need a trait, and if you can avoid the inconvenience, well why not? (2) is the standard way to go. (3) is, as I said, useful if you have reached David Tolnay level or if you're making a tutorial. Or for something else that I cannot forsee with my limited async experience.


The "if you are David Tolnay" is not meant as a jibe at anyone. In a recent Rust survey, quite literally, the highest level of Rust expertise was labelled as David Tolnay. So, it is just a funny way of saying that you are an uber expert in Rust .

9 Likes

I have a RPC-like library that uses an async trait as an application handler on the server side, and I use the async-trait crate for this. The way I look at it is that it's an acceptable temporary solution until the language supports "native" async traits.

With that said, there are some things that may throw you for a loop -- in particular if you want to pass the instance around and have default implementations; but all of that is documented. (That is to say, do not skip reading the documentation for async-trait).

I've used crates that make you return futures manually (thrussh, for instance), and to be honest both are fine -- but I personally prefer async-trait.

Using nightly Rust, it's possible to use TAITs:

#![feature(type_alias_impl_trait)]

use std::future::Future;
use tokio::io::AsyncWriteExt as _;

trait Trt {
    type FooRet: Future<Output = i32>;
    fn foo(&self) -> Self::FooRet;
}

struct S;

impl Trt for S {
    type FooRet = impl Future<Output = i32>;
    fn foo(&self) -> Self::FooRet {
        async move {
            tokio::io::stdout()
                .write_all(b"Hello world!\n")
                .await
                .unwrap();
            77
        }
    }
}

#[tokio::main]
async fn main() {
    let s = S;
    assert_eq!(s.foo().await, 77);
}

(Playground)

Note, however:

  • The async function will not use the async keyword. Instead, you must include an async move block inside the functions body, which doesn't look very nice.
  • It's not possible in stable Rust.
  • Overall, syntax may become very unhandy, especially when lifetimes are involved, which might require the use of GATs in addition to TAITs.

Using async-trait might be the better way to go in many scenarios (but comes with runtime overhead).

Another alternative is to explicitly specify the returned future (without impl). But that requires you to manually implement the future. This is what tokio does. For example tokio::io::AsyncWriteExt::write_all returns a WriteAll future, which is defined here. But that's in most cases not suitable for high-level code, I think.

I don't know what other people use. I think it depends on what your goals are. I decided on a case-by-case basis which approach I use.


To provide an extended example that requires capturing a lifetime:

#![feature(generic_associated_types)]
#![feature(type_alias_impl_trait)]

use std::future::Future;
use tokio::io::AsyncWriteExt as _;

trait Trt {
    type FooRet<'a>: Future<Output = i32>
    where
        Self: 'a;
    fn foo(&self) -> Self::FooRet<'_>;
    fn bar(&self) {}
}

struct S;

impl Trt for S {
    type FooRet<'a> = impl Future<Output = i32>;
    fn foo(&self) -> Self::FooRet<'_> {
        async move {
            tokio::io::stdout()
                .write_all(b"Hello world!\n")
                .await
                .unwrap();
            self.bar();
            77
        }
    }
}

#[tokio::main]
async fn main() {
    let s = S;
    assert_eq!(s.foo().await, 77);
}

(Playground)

1 Like