Naming the return type of an async function

With async functions coming closer to stabilization I think this is an important to discuss. Under the current proposal if you have an async fn foo() -> usize it gets desugared to an fn foo() -> impl Future<Output = usize>. This has the problem that all -> impl Trait functions have of having an innameable return type. The solution for normal functions comes in the form of existencial type. Writen using the syntax proposed here we can write

type FooReturn = impl Trait;
fn foo -> FooReturn { /**/ }

instead of

fn foo() -> impl Trait { /**/ }

as the author of the function to name the return type FooReturn. Because we don’t write -> impl Future<Output = usize> but -> usize in the current proposal this solution does not work for async functions and we will later have to create some special syntax for naming the return type in this way.

I feel like this is an important questing discussed here and that we should think of a solution for this before we stabalize async foo() -> usize. To avoid blocking in this we could initially only allow async blocks. Those have the same power as async function because the async function

async fn foo() -> usize { /**/ }

is just sugar for

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

which is a normal function with its body wrapped in an async block.

Personally I feel like stabilizing -> T at this time without looking at things like this would be a mistake. I feel like using -> T for a function that does not return T is confusing and that the unnameable return type is a symptom of a larger issue. This issue is that we are special casing these functions in a way where we will keep having to come up with workarounds for things that already work for normal functions. An other example of this is specifying additional trait bounds. @withoutboats proposed a while back to use

async(Sync) fn foo() -> usize

for an async function that returns impl -> Future<Output = usize> + Send. With the outer return type this does not have to be a special case at all as we can just write

async fn foo() -> impl Future<Output = usize> + Send { /**/ }
// or even to make the return type nameable
type FooReturn = impl Future<Output = usize> + Send; 
async fn foo() -> FooReturn { /**/ }
4 Likes

Discussion about this from the RFC itself:

And the conversation also talked about it, such as

I don’t actually see any text in that section of the RFC mentioning the naming problem.

On the discussion thread, the one comment I see addressing it head-on is https://github.com/rust-lang/rfcs/pull/2394#issuecomment-379704976, and no one seems to have objected to it. So it sounds like the position is that the nameability problem is not a blocking issue for regular async fns since it’s no different from regular impl Trait, and it only becomes a blocking issue for async fn in traits just as it’s blocking for impl Trait in traits. Does that sound right?

The problem is that the solution for naming for normal functions using existensial types is

type FooReturn = impl Trait;
fn foo() -> FooReturn { /**/ }

which does not work for async functions as

type FooReturn = impl Future<Output = usize>;
async fn foo() -> FooReturn { /**/ }
// desugars to
type FooReturn = impl Future<Output = usize>;
fn foo() -> impl Future<Output = FooReturn> { async { /**/ } }

and

type FooReturn = usize;
async fn foo() -> FooReturn { /**/ }
// desugars to
type FooReturn = usize;
fn foo() -> impl Future<Output = FooReturn> { async { /**/ } }

both of which still do not give the return type of the function.

Existential types have already been proposed and accepted

The first issue was closed as a duplicate of the second, now the second tracks both impl Trait and existential types.


You shouldn't need to put impl Future for async functions

This also does not work, see my edit to my previous reply. One solution for this would be to allow a way to name functions themselves. If we can not only call foo but can also use foo itself or something like typeof foo for well the type of the function foo (each function defines its own type that implements the Fn traits) we could do foo::Output or (typeof foo)::Output where Output is the type defined by the Fn traits. Being able to name functions themselves is a problem which I think we do want to solve eventually regardless.

async_fn!(my_fn(args...) -> T {...})

can expand to

my_fn(args...) -> T { async {...} }

I don’t know if this is good enough though.

You should be able to do:

type FooRet = impl Future<Output = T>;
async fn foo() -> T { .. }

fn __foo_FooRet() {
    let _: FooRet = foo();
}

This will assign the return of foo to the existential type FooRet.

6 Likes

One point is that async fn foo(...) -> T is not semantically equivalent to fn foo(...) -> impl Future<Output=T>.

The former, indicates that “this function should return immediately unless within a async block, with maximum cost of a struct constructor over the arguments”, and the later can do what ever it wanted to do before return.

This might not be implemented as a language restriction, but as long as the async keyword is appearing in the API, people will use this as documentation note of different behaviour guarantee.

This is slightly incorrect:

async fn's coroutine desugaring will always be exactly one struct construction and then returning that impl Future. It's only on the awaitting that it does anything else; it does the same thing when called in sync and async contexts.

(Side note this made me think about: #[inline] on async fn should probably just not exist, as it doesn't and can't mean anything.)

1 Like

I would very much like to have typeof in the language, for this and many other reasons. I’d love to have typeof(foo)::Output and typeof(foo)::Output::Output (the latter giving the output type of the Future that foo returns).

4 Likes

That is an interesting way to do it I did not think of. I think this would work so we would at least have some way to do this. In the long term however I would not like this to be the only way to do this as you have to define a dummy function and because it would not show in the documentation that this is the return type of this function. A typeof definitely seems like a good long term solution for this problem and others. We could even think of a postfix foo.type now we are discussing postfix operators to avoid having a keyword that actually consists of two words (type and typeof are both already reserved as keywords so we could do this in this edition).

I think this is the right solution. Mind, I’m strongly in favor of being able to name the types of free functions without going through a decltype mechanism, which I’ve found is a pretty kludgey solution to the equivalent problems in C++. I think typeof is a good idea for other contexts (like macros or as a stopgap for naming some unpronounceable type until we get around to it).

I’m surprised no one has suggested the simplest solution:

type FooRet = impl Future<Item = usize>;

#[verbatim_return]
async fn foo() -> FooRet {}

Where #[verbatim_return] applied to any async fn just says “don’t change the return type in the desugaring”. (Bikeshedding needed; e.g. #[actual_return].)

Alternately you could wrap the return type in a generic lang-item that gets erased/unwrapped by the desugaring:

type FooRet = impl Future<Item = usize>;

// written
async fn foo () -> Verbatim<FooRet> {}

// rendered in docs, and the actual semantics
async fn foo() -> FooRet {}

Where when the async fn desugar sees Verbatim<_> as a return type it replaces it with the type parameter. Or, it could be an actual type in the stdlib that forwards the trait impl and the compiler copies it unchanged (less surprising but limited to traits implemented for the type).

Either of these could be applied to generators as well.

It also wouldn’t be too difficult to add nice diagnostics on return type mismatches, like "oh did you mean to use the return type verbatim? Use #[verbatim_return]"

1 Like

An even simpler solution already exists, do the async fn to async block desugaring manually. It’s not just the return type that needs to be verbatim, it’s the whole function signature:

async fn foo<T: Into<String>>(bar: &usize, baz: T) -> Path {
    unimplemented!()
}

type FooRet<'a, 'b, T: 'b> = impl Future<Output = Path> + 'a + 'b;
fn foo<'a, 'b, T: Into<String> + 'b>(
    bar: &'a usize,
    baz: T,
) -> FooRet<'a, 'b, T> {
    async { unimplemented!() }
}
5 Likes

That’s definitely an option but it’s not exactly the cleanest.

It’s about as clean as it can be without introducing a specific feature for naming the return type after the async transform has modified it. With the verbatim return types you propose how would you connect the lifetimes up?

type FooRet<'a, 'b, T: 'b> = impl Future<Item = usize> + 'a + 'b;

#[verbatim_return]
async fn foo<T: Into<String>>(bar: &usize, baz: T) -> FooRet<T>

async fn foo<T: Into<String>>(bar: &usize, baz: T) -> Verbatim<FooRet<T>>
2 Likes

It’s about the same amount of work to just have the attribute preserve the whole signature, I’m leaning much more toward that than the lang-item return type anyway.

Instead of using attributes, would it be better to extend the language a bit to allow

type FooRet = MyFutureType; //My brilliant future type! Item=usize
fn foo() -> FooRet as async usize {}

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