Pre-Pre-RFC: async methods & bounding async fns


#21

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:

Current:

// declaration
async(Send) fn foo(&self) -> i32;
// definition:
async(Send) fn foo(&self) -> i32 { self.x }
fn foo(&'a self) -> impl Future<Output=i32> + Send + 'a { init(); async { self.x } }

Corresponding proposed syntax, slightly modified from my first post:

// declaration
fn foo(&self) -> async(Send) i32;
// definition
fn foo(&self) as async(Send) i32 { self.x }
fn foo(&self) -> async(Send) i32 { init(); async { self.x } }
// in all cases `async(Send) i32` desugars to `impl Future<Output=i32> + Send + 'a`.

Edit: (removed stream example, since it doesn’t add much)


#22

fn foo() where foo: Bar looks really odd, as if it was where fn(): Bar.

Could the return type be clarified with a keyword? e.g. fn foo() where return: Bar or where foo::return: Bar?


#23

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.


#24

@Nemo157 How would this work for methods? And generics? (Edit: Should work for generics no problem)

I really like the func::Output notation. I want it to work. I think it’s the cleanest notation for this feature (unless unforeseen problems pop up).

Edit: Also, is there a reason not to have this for all functions? I think it’d be quite useful.


#25

One question is whether you intend to allow implementing the desugared trait, would this successfully compile:

trait Trait {
     async fn foo(&self) -> i32;
}

// trait Trait {
//      type foo<'a>: Future<Output = i32>;
//      fn foo(&self) -> Self::foo<'_>;
// }

struct Ready<T>(T);
impl Future for Ready { ... }

struct Foo;

impl Trait for Foo {
    type foo<'a> = Ready<i32>;
    fn foo(&self) -> Self::foo<'_> { Ready(5) }
}

or, would you have to separate the async fn and the concrete future when implementing it:

impl Foo {
    fn foo_concrete(&self) -> Ready<i32> { Ready(5) }
}

impl Trait for Foo {
    async fn foo(&self) -> i32 { await!(self.foo_concrete()) }
}

#26

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 :laughing:. (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.)


#27

should be fn foo<T>() -> foo<T>::Output

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.


#28

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? :face_vomiting:

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


#29

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

#30

A few points of fact to help guide the discussion:

  1. Generics on abstract types are fine (not modules though, as discussed above).
  2. 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 :slight_smile: )


#31

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.


#32

“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>)


#33
  • 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:

<typeof foo<T>>::Output

foo<T>::Output

#34

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? :slight_smile:

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.


#35

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.


#36

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 Trait {
     async fn foo(&self) -> i32;
}

fn bar<T: Trait>(baz: T) -> i32 {
    let qux: <baz as Trait>::foo = baz.foo();
    qux.wait()
}
aside

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

trait Trait {
     async fn foo(&self) -> i32;
}

fn bar<T: Trait>(baz: T) {
    let qux: <typeof <baz as Trait>::foo>::Output = baz.foo();
    qux.wait()
}

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();

#37

Seems like it would be

<typeof T::foo>::Output

If you needed to fully specify foo for some reason, it would be

<typeof <T as Trait>::foo>::Output

If you actually needed to do it in terms of the type of baz for some reason, it would be

<typeof <typeof baz as Trait>::foo>::Output

but I can’t see this being common.

You can use the associated items (like foo and Output) without as as long as they aren’t ambiguous in the generic context.


#38

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.


#39

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.


#40

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:

existential type FooOutput: Trait;

fn _foo(a: i32) -> FooOutput { foo(a) }

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 fn should 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.