The Async Trait-or and the Case for BoxSyncFuture
1 A Tale of Poor Ergonomics
Our story begins with poor ergonomics. We don't have currying in Rust. For example, in Haskell I can do:
ghci> let addOne = (+ 1)
ghci> addOne 5
6
ghci> let addOne = (+ 1)
ghci> addOne 5
6
This is a function application. We apply
+
+
to one and get back a function that adds one to its input.
If we wish to do this in Rust, we can try first with a closure:
let add_one = |x| {
x + 1
}
let add_one = |x| {
x + 1
}
This is great! …Until we need to store a function in a struct. We can't! The closure type is unique, and we can't represent it in Rust code. What's the work around? Trait objects!
Fn|FnMut|FnOnce
Fn|FnMut|FnOnce
are traits that closures implement. So, how do we use this to represent functions at compile time? Sadly, we can't yet. The proposal use
impl
impl
in types is still a WIP (TODO link). Ah, but instead we may use traits at runtime to solve our problem. We can cast our closure into a boxed trait object and pass that around!
use std::ops::{Add, Deref};
pub struct AddSomething<T: Add>(Box<dyn Fn(T) -> T>);
// trickiness to make `add_one` callable by
// getting the compiler to automatically dereference
// the type
impl<T: Add> Deref for AddSomething<T> {
type Target = dyn Fn(T) -> T;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
pub fn main() {
let add_one = AddSomething(Box::new(|i| { i + 1}));
println!("add 1 to 5 is {}", add_one(5));
}
use std::ops::{Add, Deref};
pub struct AddSomething<T: Add>(Box<dyn Fn(T) -> T>);
// trickiness to make `add_one` callable by
// getting the compiler to automatically dereference
// the type
impl<T: Add> Deref for AddSomething<T> {
type Target = dyn Fn(T) -> T;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
pub fn main() {
let add_one = AddSomething(Box::new(|i| { i + 1}));
println!("add 1 to 5 is {}", add_one(5));
}
Alas, this is…not particularly great. It's hard to read, verbose to write, and generally annoying to maneuver.
2 The Woes of Async
Let's increase the complexity! What if we want to have our function be async? Let's say we're returning a future that sleeps for a period of time. How do we do this? Well, first we need to pull in an async executor. Let's pull in tokio. A straw man attempt might be as follows:
#[tokio::main]
async fn main() {
let f = async |x| {
tokio::time::sleep(x).await;
}
}
#[tokio::main]
async fn main() {
let f = async |x| {
tokio::time::sleep(x).await;
}
}
Sadly, we get an error:
error[E0658]: async closures are unstable
error[E0658]: async closures are unstable
So, now what? Is there a way around this? Well, we can take inspiration from the
async-trait
async-trait
crate and manually make our function async. Consider the following two functions:
async fn example_1() {}
use std::pin::Pin;
use std::future::Future;
fn example_2() -> Pin<Box<dyn Future<Output = ()> + Send + Sync>>{
Box::pin(async { () })
}
async fn example_1() {}
use std::pin::Pin;
use std::future::Future;
fn example_2() -> Pin<Box<dyn Future<Output = ()> + Send + Sync>>{
Box::pin(async { () })
}
These functions subtly different.
example_1
example_1
returns an
impl Future
impl Future
, so the type will be known at compile time, whereas
example_2
example_2
boxes and pins the future, so its type will only be known at run time. We need this to be able to represent the type in other type definitions.
And, note the trait bounds. The output of
example_2
example_2
will be
Send
Send
and
Sync
Sync
iff it is possible (e.g., if the future itself is
Sync
Sync
and
Send
Send
,
Pin<Box<...>>
Pin<Box<...>>
will be
Send
Send
and
Sync
Sync
). This is useful because now, both the returned future and references to this future can be passed between threads.
Note:
example_2
example_2
's type signature is particularly bad. The
future
future
's crate has two type aliases to make this easier:
type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>
type BoxLocalFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>
type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>
type BoxLocalFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>
Note that in the
BoxLocalFuture
BoxLocalFuture
case, the
Send
Send
bound is missing, so the future cannot be sent between threads. Both types don't add
Sync
Sync
. This makes sense, as some futures are not
Sync
Sync
, but this will definitely bite us later on.
To make our function async, we need to use a
BoxFuture
BoxFuture
:
use futures::future::BoxFuture;
use std::sync::Arc;
use std::ops::Deref;
use std::time::Duration;
use futures::FutureExt;
// millis + secs
#[derive(Clone)]
// arc is a bit more ergonomic for async code than Box is
pub struct GenTimer(Arc<dyn Fn(u64, u32) -> BoxFuture<'static, ()>>);
impl Deref for GenTimer {
type Target = dyn Fn(u64, u32) -> BoxFuture<'static, ()>;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
#[tokio::main]
async fn main() {
let gen_timer =
GenTimer(Arc::new(|secs, millis| {
async move {
tokio::time::sleep(Duration::new(secs, millis)).await
}
.boxed() // NOTE using `boxed` from `futures::FutureExt` to box and pin up the future.
}));
gen_timer(1, 1).await;
println!("completed the timer!");
}
use futures::future::BoxFuture;
use std::sync::Arc;
use std::ops::Deref;
use std::time::Duration;
use futures::FutureExt;
// millis + secs
#[derive(Clone)]
// arc is a bit more ergonomic for async code than Box is
pub struct GenTimer(Arc<dyn Fn(u64, u32) -> BoxFuture<'static, ()>>);
impl Deref for GenTimer {
type Target = dyn Fn(u64, u32) -> BoxFuture<'static, ()>;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
#[tokio::main]
async fn main() {
let gen_timer =
GenTimer(Arc::new(|secs, millis| {
async move {
tokio::time::sleep(Duration::new(secs, millis)).await
}
.boxed() // NOTE using `boxed` from `futures::FutureExt` to box and pin up the future.
}));
gen_timer(1, 1).await;
println!("completed the timer!");
}
Cool, this works!
3 Spam Sync
Suppose we want to send our timer generating function to a different task. Something like:
#[tokio::main]
async fn main() {
let gen_timer =
GenTimer(Arc::new(|secs, millis| {
async move {
tokio::time::sleep(Duration::new(secs, millis)).await
}
.boxed() // NOTE using `boxed` from `futures::FutureExt` to box and pin up the future.
}));
tokio::task::spawn(
async move {
gen_timer(1, 1).await;
println!("completed the timer!");
}
);
}
#[tokio::main]
async fn main() {
let gen_timer =
GenTimer(Arc::new(|secs, millis| {
async move {
tokio::time::sleep(Duration::new(secs, millis)).await
}
.boxed() // NOTE using `boxed` from `futures::FutureExt` to box and pin up the future.
}));
tokio::task::spawn(
async move {
gen_timer(1, 1).await;
println!("completed the timer!");
}
);
}
We get the error:
error: future cannot be sent between threads safely
--> src/main.rs:30:9
|
30 | / async move {
31 | | gen_timer(1, 1).await;
32 | | }
| |_________^ future created by async block is not `Send`
|
= help: the trait `Sync` is not implemented for `(dyn Fn(u64, u32) -> Pin<Box<(dyn futures::Future<Output = ()> + std::marker::Send + 'static)>> + 'static)`
note: captured value is not `Send`
--> src/main.rs:31:13
|
31 | gen_timer(1, 1).await;
| ^^^^^^^^^ has type `GenTimer` which is not `Send`
note: required by a bound in `tokio::spawn`
--> /playground/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.29.0/src/task/spawn.rs:166:21
|
166 | T: Future + Send + 'static,
| ^^^^ required by this bound in `spawn`
error: future cannot be sent between threads safely
--> src/main.rs:30:9
|
30 | / async move {
31 | | gen_timer(1, 1).await;
32 | | }
| |_________^ future created by async block is not `Send`
error: future cannot be sent between threads safely
--> src/main.rs:30:9
|
30 | / async move {
31 | | gen_timer(1, 1).await;
32 | | }
| |_________^ future created by async block is not `Send`
|
= help: the trait `Sync` is not implemented for `(dyn Fn(u64, u32) -> Pin<Box<(dyn futures::Future<Output = ()> + std::marker::Send + 'static)>> + 'static)`
note: captured value is not `Send`
--> src/main.rs:31:13
|
31 | gen_timer(1, 1).await;
| ^^^^^^^^^ has type `GenTimer` which is not `Send`
note: required by a bound in `tokio::spawn`
--> /playground/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.29.0/src/task/spawn.rs:166:21
|
166 | T: Future + Send + 'static,
| ^^^^ required by this bound in `spawn`
error: future cannot be sent between threads safely
--> src/main.rs:30:9
|
30 | / async move {
31 | | gen_timer(1, 1).await;
32 | | }
| |_________^ future created by async block is not `Send`
We need the function contained in
GenTimer
GenTimer
to be guaranteed to implement
Sync
Sync
so that we can pass around the pointer to the function (that is, the
Arc<dyn...>
Arc<dyn...>
. To do this, the future returned by the function must also be
Sync
Sync
. Let's spam Sync:

4 The Betrayal
Consider the following example, but inside our future we also call an async function defined in a trait. TODO