In this post, we will explore a little bit the interaction between async Rust and associated types.
Associated types
First let's see a simple example with an associated type:
#![allow(unused)]
#![allow(dead_code)]
pub trait SomeTrait {}
struct SomeStruct;
impl SomeTrait for SomeStruct {}
pub trait AnotherTrait {
// SomeAssociatedType must be a type that
// implements SomeTrait
type SomeAssociatedType: SomeTrait;
}
struct AnotherStruct;
impl AnotherTrait for AnotherStruct {
// SomeStruct is a type that implements SomeTrait
type SomeAssociatedType = SomeStruct;
}
// And we can call it with dynamic dispatch like this
fn somefunc1(x: &dyn AnotherTrait<SomeAssociatedType = impl SomeTrait>) {}
fn somefunc2(x: AnotherStruct) {
// AnotherStruct implements AnotherTrait therefore
// it can be coerced to dyn
somefunc1(&x);
}
Implement a trait on boxed dyn
Now let's change the code a little to use Box<dyn SomeTrait>
, the reason will be clear later.
#![allow(unused)]
#![allow(dead_code)]
pub trait SomeTrait {}
struct SomeStruct;
pub trait AnotherTrait {
type SomeAssociatedType: SomeTrait;
}
// dyn SomeTrait is a type and so is Box<dyn SomeTrait>
// therefore we can implement the trait for it
impl SomeTrait for Box<dyn SomeTrait> {}
impl AnotherTrait for SomeStruct {
// Now Box<dyn SomeTrait> is a type that implements SomeTrait
// so we can use as an associated type
type SomeAssociatedType = Box<dyn SomeTrait>;
}
// Same as before
fn somefunc1(x: &dyn AnotherTrait<SomeAssociatedType = impl SomeTrait>) {}
fn somefunc2(x: SomeStruct) {
// SomeStruct implements AnotherTrait therefore
// it can be coerced to dyn
somefunc1(&x);
}
Async is syntatic sugar
Let's remember that async fn
is syntatic sugar for impl Future
:
#![allow(unused)]
#![allow(dead_code)]
async fn somefunc1() -> String {
"".to_string()
}
// could be written as
// cargo clippy will even warn us that this can be
// simplified to be like somefunc1()
fn somefunc2() -> impl Future<Output = String> {
async { "".to_string() }
}
async fn callfuncs() {
somefunc1().await;
somefunc2().await;
}
Service pattern from the Tower crate
With this knowledge, we can now play with the Service pattern of the Tower crate (but in a simplified version):
#![allow(unused)]
#![allow(dead_code)]
use std::pin::Pin;
pub trait Service {
type Response;
type Future: Future<Output = Self::Response>;
fn call(&mut self, req: Vec<u8>) -> Self::Future;
}
struct EchoService;
// This is done already for us!
// impl Future for Pin<Box<dyn Future>> {...}
impl Service for EchoService {
type Response = Vec<u8>;
type Future = Pin<Box<dyn Future<Output = Self::Response>>>;
fn call(&mut self, req: Vec<u8>) -> Self::Future {
let fut = async { req };
Box::pin(fut)
}
}
async fn somefunc() {
EchoService.call(vec![0x00]).await;
}
Anonymous associated types
An impl SomeTrait
inside a trait is an anonymous associate type:
#![allow(unused)]
#![allow(dead_code)]
pub trait SomeTrait {}
struct SomeStruct;
impl SomeTrait for SomeStruct {}
pub trait AnotherTrait {
fn somefunc(&self) -> impl SomeTrait;
}
struct AnotherStruct;
impl AnotherTrait for AnotherStruct {
fn somefunc(&self) -> impl SomeTrait {
SomeStruct
}
}
// Sadly, anonymous associated types are
// not dyn-compatible, so this would be
// a compiler error
// fn somefunc1(x: &dyn AnotherTrait) {}
Async functions in traits are anonymous associate types
Then it actually follows that async functions inside traits are syntatic sugar for anonymous associate types:
#![allow(unused)]
#![allow(dead_code)]
pub trait SomeTrait {
fn somefunc(&self) -> impl Future<Output = ()>;
// This would also work here, but will give a warning
// as has some implications with auto trait bounds
// (see the compiler warning message)
// async fn somefunc(&self);
}
struct SomeStruct;
impl SomeTrait for SomeStruct {
async fn somefunc(&self) {}
}
// Is equivalent to this:
// (and the clippy would even warn us that it can simplified)
// impl SomeTrait for SomeStruct {
// fn somefunc(&self) -> impl Future<Output = ()> {
// async {}
// }
// }
// and because of the anonymous associated type, it's also
// not dyn-compatible, this won't compile:
// fn somefunc1(x: &dyn SomeTrait) {}
So one of the advantages of using named associated types (like service pattern from Tower crate do) is being able to use dynamic dispatch.
Service pattern revisited
Going back to the service pattern, it would be nice if we could avoid the Box allocation. One way is to that is by using rustc nightly (i.e.: cargo +nightly build
) and the impl_trait_in_assoc_type
feature gate:
// Note the feature gate here
#![feature(impl_trait_in_assoc_type)]
#![allow(unused)]
#![allow(dead_code)]
use std::pin::Pin;
pub trait Service {
type Response;
type Future: Future<Output = Self::Response>;
fn call(&mut self, req: Vec<u8>) -> Self::Future;
}
struct EchoService;
impl Service for EchoService {
type Response = Vec<u8>;
// this requires the rustc nightly with the
// impl_trait_in_assoc_type feature gate
type Future = impl Future<Output = Vec<u8>>;
fn call(&mut self, req: Vec<u8>) -> Self::Future {
let fut = async { req };
Box::pin(fut)
}
}
async fn somefunc() {
EchoService.call(vec![0x00]).await;
}
Note that this won't work though:
#![feature(impl_trait_in_assoc_type)]
#![allow(unused)]
#![allow(dead_code)]
pub trait Service {
type Response;
type Future: Future<Output = Self::Response>;
}
struct EchoService;
impl Service for EchoService {
type Response = Vec<u8>;
type Future = impl Future<Output = Vec<u8>>;
}
Because a concrete type somewhere is necessary to infer what is the concrete type that implements the Future
(previously the call
function), i.e., similar to how the body of this function is used to infer what is the concrete type that implements Send
:
fn _somefunc() -> impl Send {
"".to_string()
}
Manually implement Future
Another way to avoid allocations is to manually implement Future
. In this case we can implement one that immedially returns Poll::ready
:
#![allow(unused)]
#![allow(dead_code)]
use std::{
pin::Pin,
task::{Context, Poll},
};
pub trait Service {
type Response;
type Future: Future<Output = Self::Response>;
fn call(&mut self, req: Vec<u8>) -> Self::Future;
}
struct MyReturnedFuture(Vec<u8>);
impl Future for MyReturnedFuture {
type Output = Vec<u8>;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(self.0.clone())
}
}
struct EchoService;
impl Service for EchoService {
type Response = Vec<u8>;
type Future = MyReturnedFuture;
fn call(&mut self, req: Vec<u8>) -> MyReturnedFuture {
MyReturnedFuture(req)
}
}
async fn somefunc() {
EchoService.call(vec![0x00]).await;
}