Idea of how to bound types that async methods return

One solution which can help there is adding a typeof operator which Operates in both type and variable namespaces and resolves to a type of an expression which it is provided with.

Usage:

trait A {
    async fn b() where typeof(b)::Output: Send
}

Definetly an RFC, but aside of that: how to handle cycles:

let a = smth::new(); //what if this struct (or enum) is itself generic?
let b: typeof(a)::Assoc = a.method(); //now type of `b` depends on type of `a` and thus also on generic parameter which has to be inferred. But type of `b` itself can change the type of `a` because of inference.
1 Like

Also helps for gen fn's

I've been thinking about this more lately, and I think the real solution is to change the async fn syntax from:

async fn foo() -> usize {}

To:

async fn foo() -> impl Future<Output = usize> {}

Similar to C#. There are many other benefits to doing this, I'll open a new post when I have the time to collect my thoughts.

This already works:

fn foo() -> impl Future<Output = usize> {
    async { 0 }
}

As for ergnonomics it depends on how much of a special use case this is?

4 Likes

Sure it works, but that's not the main syntax, and we'd obviously like to be consistent across inherent and trait methods.

The problem is, this also works:

async fn foo() -> impl Future<Output = usize> {
    async { 0 }
}
1 Like

So what? Are there types that async fn isn't allowed to return? I'd be pretty upset about that. If you're referring to the fact that it isn't what the user intended, there are many things in programming that look right, but are not at all what is (usually) meant.

The point is that we can't assign a new meaning to that function signature (as proposed in this comment), because it already has an existing meaning in stable Rust.

3 Likes

Yup, it would be a breaking change, but worth it in my opinion. That's an extremely rare use-case. Again, was planning on writing something up more formally, but this syntax would make async fns in traits very straightforward, as well as allow for other things, like a nicer streaming syntax:

async fn foo() -> impl Stream<Output = usize> {
    for i in 0..100 {
        tokio::sleep(1).await;
        yield i;
    }
}

Or returning a boxed future, even linting when the compiler detects it would be better to:

async fn foo() -> BoxFuture<'static, usize> {
    // ...
    1
}

And it would be straightforward to return a named future, instead of an opaque one:

type Sleep = impl Future<Output = usize>;

async fn sleep() -> Sleep {
    tokio::sleep(1).await;
}

struct Foo {
    sleep: Sleep
}

Of course, these are all possible today, but is more involved. We would have to come up with a new lifetime 'input/'bikeshed that covers the lifetime of all input parameters:

async fn foo(&self, bar: &str, baz: &str) -> impl Future<Output = ()> + 'input {
    print(&self.bar);
    print(bar);
    print(baz);
}

If you wanted to write something like that with a boxed future today, you have to write all the lifetimes out explicitly, which isn't very nice. That's the main issue that was brought up in the RFC, and I think adding a 'input lifetime is a much better solution than hiding the output future type entirely, as it's lead to issues like the one we face now with bounding async trait, and it leads to beginner confusing as async fn doesn't tell you anything about lifetimes.

1 Like

Regarding it being a breaking change, I think it's something that could change across an edition, and it would be relatively simple to cargo fix.

Returning other impl Traits however is not so rare, and your proposal would make them pretty inconsistent with how impl Future and impl Stream will be handled.

This has nothing to do with your proposal, rather it only requires the type-alias-impl-trait feature.

This is pretty much reworking all the impl Trait in return positions.

I don't understand what this has to do with hiding the output future type. You can already write the explicit impl Future form, the only difference with your proposal seems to be that you also want to require the async keyword in front of the function signature and to add a magic 'input lifetime.

1 Like

Returning other impl Trait s however is not so rare, and your proposal would make them pretty inconsistent with how impl Future and impl Stream will be handled.

How would it make them inconsistent? With this proposal, async fn is simply sugar for fn ... { async { ... } }.

This has nothing to do with your proposal, rather it only requires the type-alias-impl-trait feature.

Yes, it does require the type-alias-impl-trait feature, but with this proposal it makes returning a named existential type a simple change, and consistent with the opaque type version. If we wanted a simple way to do this with the current change, we would have to extend the syntax to something like async(MyNamedFuture) fn foo.

This is pretty much reworking all the impl Trait in return positions.

How so?

I don't understand what this has to do with hiding the output future type. You can already write the explicit impl Future form, the only difference with your proposal seems to be that you also want to require the async keyword in front of the function signature and to add a magic 'input lifetime.

The chosen solution was to hide the output type and do the lifetime desugaring in the async fn transform, instead of having the user write it out.

Imagine my proposal was the chosen syntax for async fns. Future extensions would be made much easier, such as bounding async fns in traits, streaming syntax, etc. With the current syntax, we're limited in that the future type is hidden.

Again, I haven't had time to write this out more clearly yet, but there's an older thread with similar ideas if anyone is interested: Pre-Pre-RFC: async methods & bounding async fns - #63 by MajorBreakfast, and a nice summary of the argument here: rust-blog/2018-06-19-outer-return-type-approach.md at master · MajorBreakfast/rust-blog · GitHub

Because in async fn foo() -> impl Trait { ... } the impl Trait would refer to the output type of the Future, while in async fn foo() -> impl Future { ... } the impl Future would refer to the Future itself. Same syntax, but different traits gets different semantic meaning.

If that's all it changes then async fn foo() -> impl Trait { ... } would stop working, because it would be desugared to fn foo() -> impl Trait { async { ... } } which doesn't work unless the Future itself implements Trait rather than its Output type.

1 Like

Because in async fn foo() -> impl Trait { ... } the impl Trait would refer to the output type of the Future , while in async fn foo() -> impl Future { ... } the impl Future would refer to the Future itself. Same syntax, but different traits gets different semantic meaning.

You're misunderstanding. All traits get the same semantic meaning. async fn foo() -> impl Trait would not be allowed, because that would desugar to fn foo() -> impl Trait { async { ... } }. Down the line support could be added for other types, like streams, or boxed futures.

If that's all it changes then async fn foo() -> impl Trait { ... } would stop working

Yes, exactly, that's my point :smiley:

I ended up writing things out more clearly and opened a new thread here: An Alternative Syntax for Async Functions

ISWIM introduced where to qualify what was said before. Is there a compelling reason for where after an fn signature to be unable to spell out conditions on the return type as well as on argument types?

There has been no way so far to name the return type in a where clause. Isn't that the reason this has never been done yet?

(did you post the wrong link?)

(Think that's the right one.. 1966 paper by P. J. Landin which first introduced where clauses)

P.S. well I guess after some consideration I will have to dress this as a kind of a joke 'cause indeed Landin's where is different from Rust's - for one thing it creates and environment in which the "main" definition operates; it doesn't "use" it. It is however an interesting fact that this is where where has gained its popularity from - from this very article. And of course my other point stands - I might be missing something but I don't see why where cannot specify extra conditions on both input and return types of an fn.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.