Sorry if I came across as snippy. ^_^; I very much understand that the current proposals do not allow for async in return position. But to suggest ways to ease usage of exactly those tricky lifetimes for futures, but also other closures later on (e.g., generators), was the motivation behind my post.
Now, perhaps the boat has sailed, and it's too late to make such suggestions. And perhaps I'm missing some implementation details that make things more difficult. I would just like to ideally arrive at a syntax that is easy(-ier) to understand and easier to extend. So please allow me one last summary:
Supporting the foo::Output form that has been mentioned above seems quite easy, itâs just a small extension to the proposed desugaring:
async fn foo() -> i32
/* desugars to: */
mod foo {
pub abstract type Output: Future<Output = i32>;
}
fn foo() -> foo::Output
It appears to have the same potential for conflicts as the proposal, type aliases and modules share the same namespace, but I canât think of any likely ones off the top of my head.
Umm, it wouldn't, I somehow completely forgot to consider them. I don't think it would work with generics either, you would want something like
async fn foo<T>(x: T) -> T
/* desugars to: */
mod foo<T> {
abstract type Output: Future<Output = T>;
}
fn foo<T>() -> foo<T>::Output
which isn't possible without generic modules. We just need to RFC and implement them, along with "modules in Traits and impls" to support methods, then we're golden . (Obviously I'm kidding, while I like the syntax foo::Output I don't think that would work well if there's no way to extend it to methods, which I think really requires using typeof somewhere in there.)
I'd prefer if it was just compiler magic. ::Output shouldn't be unique to async functions. It should work also for all other functions. In particular functions that return Future. The proposal currently introduces a type that would probably make adding this feature in the future impossible.
Desugaring async fn foo() into mod foo { type Output = ..; } has two other problems:
You canât put a mod inside a trait, only a bare type, as @Nemo157 mentions.
How do you talk about the Output of the Future itself? foo::Output::Output?
An async fn declaration really is a type declarationâunlike functions using impl Future that can just switch to abstract type to name their return type directly, async fns have nowhere to âhook upâ such an abstract type and thus need it to be built-in. And just like tuple structs, the only unique name for such a type is the name of the declaration itself.
On the other hand, @MajorBreakfast, this does not make correspondence between -> impl Future and async fn impossible. Nothingâs stopping you from writing an abstract type with the same name as the function that returns it, or even somehow automating that (a derive, building it into the language (why??), etc).
This might look a bit odd at first, but it has actual logic to it. foo is the function, foo::Output its output, the future, and foo::Output::Output the output of the future.
Naming the future type after the function on the other hand seems not logical to me. Sure they live in different namespaces, so it works, but it's not logical. That kind of thing only makes sense for tuple structs because calling the constructor looks similar to the declaration. The FnOnce trait has already an Output associated type. As noted above, it cannot currently be accessed, but that might be a future feature. So, if anything, foo::Output should correspond to FnOnce's associated type.
I'm not saying that Rust should immediately get the ::Output feature. But, there are alternatives to @withoutboats's proposal that don't make it impossible forever:
Name the concrete type ..._output (snake_case type name. Do we want this? Current proposal has this as well)
Or introduce a macro: async_fn_output!(foo) that returns the concrete type. (Could just add a weird suffix)
Or ensure the trait bounds via an attribute
Or only introduce the sugar proposed above that ensures trait bounds: async(Unpin) fn foo() -> i32. I don't particularly like the notation, because I think trait bounds for the return type belong on the right hand side of ->. The syntax can be tweaked, though
A few points of fact to help guide the discussion:
Generics on abstract types are fine (not modules though, as discussed above).
It would be a breaking change to make every function a type name, because you can already have functions & types with the same name in the same scope (the compiler does this with the P type, for example). So its not foo::Output, weâd need a typeof mechanism to do <typeof foo>::Output.
( also a respectful request to slow the thread down so I have a hope of catching up later )
Yes it would be a breaking change but it would only affect struct/enums and functions. The compiler lints against structs/enums that are not PascalCase and functions that are not snake_case so I donât think this would be a really big issue.
I also think it is quite intuitive to address the (currently anonymous) type of a function this way. Using foo::Output should be forward compatible with adding this.
"Only"? Those are perhaps the two most-used, core features of Rust (and probably any modern programming language).
By the way, I don't understand why the return type has to have the same name as the function. That in itself is pretty confusing. The analogy to tuple structs is strange to say the least, because an an arbitrary async function has the ability to perform arbitrarily complex operations in addition to constructing a value of its return type.
As to the syntax, async(Trait) looks pretty ugly, also it seems it has to do something with the function's overall type, which it doesn't. If we used ::Output, then it would be possible to just add a where bound as with generic functions, and there would be no need for more and more special syntax. (Of which async and related features seem to be a great attractor anyway, which is kind of disappointing, given that they were supposed to be syntactic sugar for state machines and Future-returning functions⌠Blowing up the language with even more irregularity doesn't really mesh well with that goal, apart from in itself being undesirable. </rant>)
Rust 2015 could simply parse func::Output in a special way: If func is a type, it picks the type, otherwise it looks for a function called func (Edit: For all functions!)
Rust 2018 has the possibility to do it cleanly and reserve the type names for all functions. As @DDOtten mentioned there are going to be few collisions thanks to Rust's naming conventions.
<typeof foo>::Output doesn't look too bad, but it's a bit longer and can be a bit cumbersome in combination with generics:
Personally, Iâd prefer typeof, because it would also work for non-async fns â including fns that return impl Trait, fixing the inability to name their return type, or indeed any other fn whose type you want to name, which would allow impling arbitrary traits for specific fns.
typeof would also have a variety of hackier use cases, especially around macros, which seems to be the source of some opposition to having it in the language. But for one thing, whatâs wrong with hacks?
For another, I think itâll be possible eventually to implement at least a subset of typeof as a macro using const generics (once const generics supports arbitrary types). Something like this:
trait Foo<T, const val: T> {
type T;
}
impl<T, const val: T> Foo<T, val> for () {
type T = T;
}
macro_rules! type_of {
($e:expr) => { <() as Foo<_, {$e}>>::Output }
}
That wouldnât work in all cases (only for constant expressions), but the point is, eschewing typeof wonât stop hacks, just make them even hackier. Moreover, if there are currently any implementation obstacles to making typeof work (I am not sure), const generics will probably have to deal with the same issues. (FWIW, I know of at least one issue that currently affects expressions written in array lengths, and probably has to be solved for const generics, but probably wouldnât affect typeof.)
So I think we may as well have typeof, considering how handy itâs turned out to be in other languages; and then we may as well use it for this.
One issue is that with a true typeof, you'd need turbofish: <typeof foo::<T>>::Output
Spitball proposal: In a future edition, make all fns (not just async ones) define a type with the same name as the fn, to make it easy to name the type; for the short term, add typeof, since it also has many other (unrelated) use cases.
One thing Iâve noticed with the typeof discussion up to this point is a lack of what it would look like with trait methods. As a comparison, hereâs what I believe would be valid under this PPRFC
Trait defines an âunstatedâ associated type foo that the implementations have to provide (Iâm really struggling to come up with an accurate descriptor for the âkindâ of this associated type, I donât think itâs quite âimplicitâ because under this PPRFC itâs relatively tightly constrained, but itâs still not âexplicitly statedâ in the code).
Comparatively if there were a typeof expr: Expression -> Type unary operator it might be
I really havenât spent much time looking at where Rust requires specifying exactly which traitâs associated type you want with as, but depending on whether or not it can decide you want FnOnce::Output there that may need to instead be
let qux: <<typeof <baz as Trait>::foo> as FnOnce>::Output = baz.foo();
Having a typeof mechanism is not mutually exclusive with this proposal. We can have that totally independent of how we handle async methods. However, its important to remember @rpjohnstâs earlier point: we allow nested existentials so that impl Trait, so typeof would not allow you to refer to the anonymous type in all cases: -> Vec<impl Debug> for example.
I also think a lot of the back and forth on this thread is demonstrative of how confusing typeof can be as a construct. While its a reasonable fallback for when you have no other option, I donât think its a good idea to encourage a mechanism that forces you to really understand how function names and function types relate to one another; most users are able to get by with only a casual understanding of this right now.
For those reason, I am more optimistic about encouraging named existentials for cases where the ability to name an existential matters. Just use abstract type! This means impl Trait in trait definitions would be uncommon in public APIs unless the API author was confident thereâd be no reason to constrain on that hidden type.
The distinction with async fn is that thereâs nowhere to put the abstract type name, so that solution isnât adequate for async fn. Hence the alternative solution proposed here.
On top of what @withoutboats describes, typeof tends to accumulate a lot of hacks. The core problem is you have to be able to come up with a value for the type you want to talk about, which is not always easy or possible, which then leads to things like C++'s declval.
The more we can stick to abstract type the betterâand that also means ::Output for arbitrary functions should be (made to be) unnecessary, along with typeof. async fns are the exception here not only because they canât use abstract type, but because they really do function as type definitions themselves.
The analogy to tuple structs is almost perfectâthe foo(a, b, c) syntax constructs a value of the foo type, and the body of the async fn is merely part of a trait implementation for the foo type.
Named existential types work for the cases where named existential types work. They don't work for async fn. A solution that works for async fn doesn't have to be ideal for the cases that are better covered by named existential types, only for the cases that aren't.
Unless there's an easy way to create a name for an existential returned from a foreign function. I suppose you could do:
This sort of leads into what @rpjohnst is talking about regarding typeof here:
This is an interesting observation, though it doesn't quite jump off the page. This basically says that async fnshould be treated differently from fn, since its actually a type + its literal constructor and not a function. This somewhat suggests a syntax more like:
async Foo(a: A, b: B, c: C) -> i32 {
...
}
but obviously the suggestion that it's like a function is also valuable. I'm not sure how much presenting async fn foo as a function is compatible with treating foo as a type + its literal constructor, but I can see the reasoning.