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.
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.
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.)
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).
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;
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
}
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;
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
.