How often do you want non-send futures?

Pin doesn't stop moving the future across threads, you can pass a Pin<Box<Foo>> across threads and poll it on multiple of them if Foo: Send (playground).

I guess that means I need to make all my Futures !Send, at least depending on some feature flag.

1 Like

This is correct. The Send auto trait assumes that things can be sent across threads if they have no access to thread-local data. (This is true for safe code, although it doesn't capture the effects of e.g. accessing thread-locals, which can cause surprising results -- but not unsafety.)

I found this comment incomprehensible but I think we are actually agreeing?

I don’t think we are. If I understood you correctly, I believe you suggested that the parser should reject:

async(Foo + Bar) fn myfun<'a>() {}

and only accept:

async(?Send) fn myfun() {}

I am suggesting that if we go down this route, we should syntactically and semantically accept:

async(Foo + Send) // modulo ?Send / Send discussion
fn bar() -> R {}

which is desugared into:

fn bar() -> impl Future<Output = R> + Foo + Send {}

via lowering, because it would be ad-hoc not to do so and also inconsistent with how ?Sized behaves.

The syntactic / semantic distinction is also relevant because Rust’s surface syntax alone has stability guarantees even if the syntax later results in semantic errors. For example:

#[cfg(FALSE)] struct A { f: impl Copy }

will compile but:

struct A { f: impl Copy }

will not, and this has stability implications.

3 Likes

I presume that B is a type in this context, but what does it refer to, the outer return type?

I think @withoutboats was just saying that initially we could be conservative but eventually aim to accept arbitrary bounds, and not to specifically say that we should reject async(?Send) in the parser (as opposed to later in lowering).

but to what end?

From my perspective, this conservatism is a complication both in terms of the grammar, time investment, and compiler complexity.

I also don’t see much reason to be conservative. (But I doubt it was the “high-order bit” for @withoutboats either – but I should let them speak for themselves.)

1 Like

No I'm saying what you're saying (but I think in the short term we should hard reject bounds other than ?Send after parsing. This is because some have more complicated implications, Unpin in particular).

2 Likes

Yes, since there’s no other way to specify any information about it. But conceptually, it should only be used to specify traits that must be satisfied by all captured state.

I like where this goes in terms of reuse of existing syntax. Possible alternative: the async keyword is also available as the generic variable directly?

async foo() -> Result<Bar, Baz>
    where async: Send;
7 Likes

In that case, I do think we should consider these complicated implications before introducing async(Send) or similar so that we can tell whether we will end up with something uniform and coherent in the end. It would be concerning would we not.

Intriguing; I concur with @dekellum that this appears to be good for reuse and I think it's conceptually simple. I wonder however whether this has any use beyond auto traits and lifetimes since the compiler generates the actual return type itself. If we had some sort of -XScopedTypeVariables for this existential specifically, I suppose you could allow:

async<R> fn foo() -> u8 where R: MyTrait {
    impl MyTrait for R {
        ...
    }

    0
}
3 Likes

I think that actually makes more sense than introducing an arbitrary type name. I like that it signals that it’s slightly different from how where clauses normally work, and I think it would actually be pretty clear what is meant upon first seeing the syntax.

On the other hand, if we allow async<R> then we can presumably allow the bound inline as well, namely:

async<R: Send> fn foo() -> u8;
3 Likes

FWIW: I’d probably use F (or even Futr) as the generic variable in that case.

I think its also better than a new async(..) syntax, but personally, I tend to favor using where incl. in existing usage, because I think its nicer to have such details on the end of the signature.

Edit: I have no problem with specifying the bounds in either position remaining a personal style choice.

I have two concerns with the async<R> syntax.

for<T> is a type variable, but async<R> really isn’t. We can and maybe should allow it to be used in ways that neither type variable or existential types can be, and if we do allow it more power that could be confusing if it uses the same syntax.

Also people have expressed a want for a way to name the return type, so we should find a way to name it that allows the name to be exported.

Why would it not be possible for async<R> to actually be a means of naming the return type variable? (Assuming that such a thing really is desirable.)

Because R isn’t a newly-introduced universal the way T is. The return type is closer to a newly-declared item, with the function as its constructor and methods.

Tuple structs are a similar case- in struct Foo(i32), Foo is both a type name and a function name. And you can already assert that a struct be Send with a where clause:

struct Foo where Foo: Send {
    x: i32, // works
    y: *const i32, // `*const i32` cannot be sent between threads safely
}

Thus, we might do the same for async fn and introduce no new syntax at all:

async fn foo(a: A, b: B) -> R where foo: Send {
    ..
}

I would interpret this as asserting that the ZST associated with the function is Send rather than the output type being Send.

6 Likes