Implied Send

Status

❌ Rejected. This idea can be quite productive, but it is not versatile (it rules out important use cases) and it is not supportive (it is confusing).

(FIXME: I think the principles aren't quite capturing the constriants here! We should adjust.)

Summary

Targets the bounding futures challenge.

The core idea of "implied Send" is to say that, by default at least, the future that results from an async fn must be Send if the Self type that implements the trait is Send.

In Chalk terms, you can think of this as a bound like

if (Implemented(Self: Send)) { 
    Implemented(Future: Send)
}

Mathematically, this can be read as Implemented(Self: Send) => Implemented(Future: Send). In other words, if you assume that Self: Send, then you can show that Future: Send.

Desugared semantics

If we extended the language with if bounds a la Chalk, then the desugared semantics of "implied send" would be something like this:


#![allow(unused)]
fn main() {
trait AsyncIterator {
    type Item;
    type NextFuture: Future<Output = Self::Item>
                   + if (Self: Send) { Send };

    fn next(&mut self) -> impl Self::NextFuture;
}
}

As a result, when you implement AsyncIterator, the compiler will check that your futures are Send if your input type is assumed to be Send.

What's great about this

The cool thing about this is that if you have a bound like T: AsyncIterator + Send, that automatically implies that any futures that may result from calling AsyncIterator methods will also be Send. Therefore, the background logging scenario works like this, which is perfect for a work stealing executor style.


#![allow(unused)]
fn main() {
async fn start_writing_logs(
    logs: impl AsyncIterator<Item = String> + Send + 'static
) {
    ...
}
}

What's not so great

Negative reasoning: Semver interactions, how to prove

In this proposal, when one implements an async trait for some concrete type, the compiler would presumably have to first check whether that type implements Send. If not, then it is ok if your futures do not implement Send. That kind of negative reasoning is actually quite tricky -- it has potential semver implications, for example -- although auto traits are more amenable to it than other things, since they already interact with semver in complex ways.

In fact, if we use the standard approach for proving implication goals, the setup would not work at all. The typical approach to proving an implication goal like P => Q is to assume P is true and then try to prove Q. But that would mean that we would just wind up assuming that the Self type is Send and trying to use that to prove the resulting Future is Send, not checking whether Self is Send to decide.

Not analogous to async fn outside of traits

With inherent async functions, we don't check whether the resulting future is Send right away. Instead, we remember what state it has access to, and then if there is some part of the code that requires a future to be Send, we check then.

But this "implied send" approach is different: the trait is effectively declaring up front that async functions must be send (if the Self is send, at least), and so you wind up with errors at the impl. This is true regardless of whether the future ever winds up being used in a spawn.

The concern here is not precisely that the result is too strict (that's covered in the next bullet), but rather that it will be surprising behavior for people. They'll have a hard time understanding why they get errors about Send in some cases but not others.

Stricter than is required for non-work-stealing executor styles

Building on the previous point, this approach can be stricter than what is required when not using a work stealing executor style.

As an example, consider a case where you are coding in a thread-local setting, and you have a struct like the following


#![allow(unused)]
fn main() {
struct MyCustomIterator {
    start_index: u32
}
}

Now you try to implement AsyncIterator. You know your code is thread-local, so you decide to use some Rc data in the process:


#![allow(unused)]
fn main() {
impl AsyncIterator for MyCustomIterator {
    async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let ssh_key = Rc::new(vec![....]);
        do_some_stuff(ssh_key.clone());
        something_else(self.start_index).await;
    }
}
}

But now you get a compilation error:

error: `read` must be `Send`, since `MyCustomIterator` is `Send`

Frustrating!