Return type annotation of async block

Currently there is no way to specify return type of a async block other than having full annotated return value. In some cases it is not that easy either, e.g. if you have

async move {
    loop {
        let item = stream.next()?;
        process(item);
    }
}

I propose that we add syntax to allow specifying return type of a async block similar to how return type can be annotated on closures:

async move -> Result<(), Error> {
    // ...
}

I briefly searched in the original RFC for async-await and the internal forum, but didn't see anything proposed for this.

What do you think about this?

5 Likes

How about

async move {
    {
        // ...
    } as Result<(), Error>
}

or with type_ascription

async move {
    {
        // ...
    }: Result<(), Error>
}

Also.. what exactly do you mean by

and why is it not desirable? Or is this just about moving a type annotation from one place to another?

Finally I don’t really like reusing a -> notation for something that is not a function or a closure (even though a future is a bit like a function).

I'm also wondering why an "immediately invoked closure" wouldn't be good enough. Unless I'm missing something, it'd be only a few extra characters over this proposal in all cases.

1 Like

It's the thing mentioned in the async book:

Ok::<(), MyError>(()) // <- note the explicit type annotation here

It's undesired because it's an unusual syntax and doesn't feel intuitive. And it's not quite doable in this case where there is an infinite loop.

This looks nice but I personally don't feel very comfortable with it.

Also I think it's really just imitating closures. There is no reason that any of the approaches you mentioned doesn't work with closures, but I still find it's clearer to use -> ReturnType for annotating return type of closure when it can't be induced otherwise.

It's probably just a personal feeling thing, though.

How do you do that? You mean something like

move || -> impl Future<Output = Result<(), Error>> {
    async move || {
        // ...
    }
}()

?

This doesn't look like "a few extra characters", and I failed to figure out anything simpler. Maybe I'm missing something. Could you elaborate?

Maybe Ok(()) as Result<(), MyError> feels more intuitive then.

The other approach using type parameters for Ok has the advantage that you can save repeating the return type:

Ok::<_, MyError>(something_with_complicated_type())

I suppose what @Ixrec might have meant was something like

async move { move || -> Result<(), Error> {
    // ...
}()}

I’m not sure if the async_closure feature includes the syntax

async move || -> Result<(), Error> {
    // ...
}()

Also note that I’m not necessarily against your idea (just against the use of an arrow). I would (personally) probably prefer it to look like this, especially also to more visibly differentiate it from the closure variant

async move Result<(), Error> {
    // ...
}

One should also consider that something like this could be useful with try blocks. In case we really want this, we should consider how this works syntactically similar for both.

1 Like

It does!

I initially thought that using an async closure would be more limiting than an async block, because you can return, break, continue and ? across regular blocks but not across closures. However, control flow in async blocks has the same limitations as in closures. So if I'm not mistaken, it's always possible to convert an async block to an immediately-invoked async closure.

2 Likes

I think it's bad idea to special-case this. Once impl Trait can be used to specify the types of locals, this problem will solve itself.

If, on the other hand, we special case this, we get clunky syntax for an even clunkier concept until the rest of time due to stability guarantees.

I believe the proper fix for this is just building out impl Trait so it can be used to its full potential.

1 Like

That would only help when you assign the value to a local variable. async-block, however, just like closures, very often used as function arguments, and the function parameters are usually generic themselves as well. For example,

tokio::spawn(async move { ... });

It's actually more tricky when it comes to try blocks. If we follow the idea of async function, the annotation should be the Future's Output type rather than the Future itself. And since try block is probably going to get Ok-wrapping, its type annotation would probably need to be the type of the Ok branch... which could be more controversial.

2 Likes

One option would be to lean into the fact that an async block is a closure to disconnect it from try blocks and tie the syntax more towards Fn-closures as proposed in the OP (there's a reason async move is a thing but try move isn't). Then, similar to Fn-closures being annotated with the type the closure returns, not the type of the closure, Future-closures should be annotated with the type the closure returns, not the type of the closure. (And async closures which are Fn-closures returning Future-closures are annotated with the final return type of both closures, not the type of either of the closures making it up).

let future
    : impl Future<Output = u32>
    = async move -> u32 { todo!() };

let function
    : impl Fn() -> u32
    = || -> u32 { todo!() };

let async_function
    : impl Fn() -> impl Future<Output = u32>
    = async move || -> u32 { todo!() };
3 Likes

If impl Trait can be used to its full potential, I don't see why those use cases would be a problem.

Mind you, it can still be some work to actually specify the types, but at least it will be possible.

I suppose one could also solve these kind of type-annotations with a method

use std::future::Future;
trait Outputting: Sized {
    fn outputting<O>(self) -> Self
    where Self: Future<Output=O>
    {
        self
    }
}
impl<T: Future> Outputting for T {}

fn foo() {
    let x = tokio::spawn(async move {1u8.into()}.outputting::<u32>());
}
3 Likes

There are lots of ways to make it possible for sure.

Rust doesn't go the Python path that everything should have only one right way, so there are lots of things which can be done otherwise still have some syntax sugar to make it either more straightforward or more ergonomic.

All your tricks can apply to closures as well, but as I mentioned before, I still believe the return type annotation of closure is nicer in general.

No it doesn't. However that shouldn't mean that just every old redundant feature can or should be thrown in, because such features tend to cruft up any language that incorporates them on the long run.

Maybe you remember that inheritance in OO languages was once heralded as a really nifty feature. But that hype didn't mean the feature was pulling its weight, and by the time folks figured that out, it was way too late for the languages that incorporated them. Add a few more of such misfeatures (e.g. enums that aren't ADTs but semi-useless sugar for classes and thus can't be matched on, or a type system that isn't quite comprehensive enough, etc) and you get Java, essentially.

If you want syntax sugar for specifying types, you can always reach for a macro. It'll work without having to burden the language with features that are not only a bad idea (my opinion) but redundant (fact).

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