Rust: Trait With Async Methods
First of all, I hope that you are familiar with Rust Trait, a type we use to define shared behavior in an abstract way. For me, I see it as an inferior version the Swift Protocol (I am sorry if you don’t agree, but the second we cannot add required properties/fields in trait, it is over)! Anyway! Use async methods with Traits can be inevitable sometimes, but depending on the use case, this can require a bit of trial and error to actually get it to work! In this article, let me share with you really quick on how we can create a trait with async methods, couple problems we might encounter and some workarounds!
Basic First of all, defining async functions in a trait does not cause any problems it self! trait Animal {
async fn eat(&self, food: &str) -> anyhow::Result<()>;
}
struct Dog;
impl Animal for Dog {
async fn eat(&self, food: &str) -> anyhow::Result<()> {
println!("eating {}!", food);
Ok(())
}
} First Problem When do those problems come in? Dynamic Dispatch! PS: If you want to read more about Dynamic Dispatch and Generics, please check out one of my previous articles Rust: Dyn (Dynamic Dispatch) vs Generics (Monomorphism / Static Dispatch)! For example, let’s add a Zoo that contains a field of a vector of Animals! struct Zoo {
pub animals: Vec<Box<dyn Animal>>,
} And unfortunately, here is what we get. When the trait contains async method, it is not dyn compatible anymore!
With Async Trait To solve our problem above, we can use this async-trait crate, a crate that provides an attribute macro to make async functions in traits work with dyn traits. All we have to do is to mark our trait and the implementation with async_trait.
struct Zoo {
pub animals: Vec<Box<dyn Animal>>,
}
- [async_trait]
trait Animal {
async fn eat(&self, food: &str) -> anyhow::Result<()>;
}
struct Dog;
- [async_trait]
impl Animal for Dog {
async fn eat(&self, food: &str) -> anyhow::Result<()> {
println!("eating {}!", food);
Ok(())
}
} Perfect! Let’s add a dummy implementation to the zoo just to confirm. impl Zoo {
async fn feed_animals(&self) -> anyhow::Result<()> {
for animal in self.animals.iter() {
animal.eat("food").await?
}
Ok(())
}
}
- [tokio::main]
async fn main() -> anyhow::Result<()> {
let zoo = Zoo {
animals: vec![Box::new(Dog)],
};
zoo.feed_animals().await?;
Ok(())
} Another Problem However, what if we have a field within the Dog that holds an array of Animal? struct Dog {
pub friends: Vec<Box<dyn Animal>>,
}
Oops! There are couple ways of solving this! Approach 1: Box Future First of all, let’s agree on this, async function produce Future. If we can write our function as async, we can convert it to a regular sync fn that returns a Future instead! So! The solution should be pretty straightforward now! Make our function sync and return this BoxFuture from the futures crate! This is actually what async-trait crate does underwood, inserting a BoxFuture and doing the boxing for us. Let’s remove those async_trait attributes and change our trait method as well as the implementation to the following. use futures::future::{ready, BoxFuture}; trait Animal {
fn eat(&self, food: &str) -> BoxFuture<'static, anyhow::Result<()>>;
} Here, I am using the 'static as my lifetime, but you can also use other lifetimes based on your needs. We can then implement the trait like following. impl Animal for Dog {
fn eat(&self, food: &str) -> BoxFuture<'static, anyhow::Result<()>> {
println!("eating {}!", food);
Box::pin(ok(())) // equivalent to Box::pin(ready(Ok(())))
}
} Or if we want to use self and call some other async functions. No problem! impl Animal for Dog {
fn eat(&self, food: &str) -> BoxFuture<'static, anyhow::Result<()>> {
if let Some(friend) = self.friends.first() {
return friend.eat(food);
}
println!("eating {}!", food);
Box::pin(ok(())) // equivalent to Box::pin(ready(Ok(())))
}
} In our Zoo, we actually don’t have to change anything and just await on that eat exactly the same as above! impl Zoo {
async fn feed_animals(&self) -> anyhow::Result<()> {
for animal in self.animals.iter() {
animal.eat("food").await?
}
Ok(())
}
} Approach 2: Implement Sync The compiler’s message already gave this out, but actually, all we have to do is to implement Sync for our Animal trait!
- [async_trait]
trait Animal: Sync {
async fn eat(&self, food: &str) -> anyhow::Result<()>;
} Yes! That’s it! By the way, this is also what we need to do if we want to spawn the eat function to some other thread, or we want to join_all for concurrent execution! If you want to find out more about those, please feel free to check out couple of my previous articles!
- Rust: Execute Multiple Async Simultaneously: Multithreading vs Futures
- Rust: Transfer Data Between Threads
Code Snippet That’s it for the day! Here are the full snippets for you to give it a try yourself! Async Trait Version use async_trait::async_trait;
- [tokio::main]
async fn main() -> anyhow::Result<()> {
let zoo = Zoo {
animals: vec![Box::new(Dog { friends: vec![] })],
};
zoo.feed_animals().await?;
Ok(())
}
struct Zoo {
pub animals: Vec<Box<dyn Animal>>,
}
impl Zoo {
async fn feed_animals(&self) -> anyhow::Result<()> {
for animal in self.animals.iter() {
animal.eat("food").await?
}
Ok(())
}
}
- [async_trait]
trait Animal: Sync {
async fn eat(&self, food: &str) -> anyhow::Result<()>;
}
struct Dog {
pub friends: Vec<Box<dyn Animal>>,
}
- [async_trait]
impl Animal for Dog {
async fn eat(&self, food: &str) -> anyhow::Result<()> {
println!("eating {}!", food);
Ok(())
}
} BoxFuture Version use futures::future::{ready, BoxFuture};
- [tokio::main]
async fn main() -> anyhow::Result<()> {
let zoo = Zoo {
animals: vec![Box::new(Dog { friends: vec![] })],
};
zoo.feed_animals().await?;
Ok(())
}
struct Zoo {
pub animals: Vec<Box<dyn Animal>>,
}
impl Zoo {
async fn feed_animals(&self) -> anyhow::Result<()> {
for animal in self.animals.iter() {
animal.eat("food").await?
}
Ok(())
}
}
trait Animal {
fn eat(&self, food: &str) -> BoxFuture<'static, anyhow::Result<()>>;
}
struct Dog {
pub friends: Vec<Box<dyn Animal>>,
}
impl Animal for Dog {
fn eat(&self, food: &str) -> BoxFuture<'static, anyhow::Result<()>> {
if let Some(friend) = self.friends.first() {
return friend.eat(food);
}
println!("eating {}!", food);
Box::pin(ok(())) // equivalent to Box::pin(ready(Ok(())))
}
}
Thank you for reading! That’s it for this article! Happy trait-ing!
Read the full article here: https://blog.stackademic.com/rust-trait-with-async-methods-a62ce5ad5be8