Why closure cannot take `impl trait` as return type

At a quick glance, I thought impl trait is syntax sugar for T: Future, but it is not the case for return type. What is the underlying reason rustc do not treat it as same?

use std::{future::Future};
fn closure_ok<F>(f: impl FnOnce() -> F)
where
    F: Future<Output = ()>,
{
}

// below is compilation erro
fn closure_err(f: impl FnOnce() -> impl Future<Output = ()>) {}
1 Like

impl Trait in argument position (APIT) is roughly the same as a generic parameter, whereas impl Trait in return position (RPIT) is an opaque alias for a single type, determined by the function body. The single type might be parameterized by any generic input types, but is not itself a generic parameter.

Instead you can use

fn closure_err<R: Future<Output = ()>>(f: impl FnOnce() -> R) {}
3 Likes

Thanks, I want to understand why PRIT cannot be implemented in rustc to work in the same way as APIT,

Where RPIT is allowed:

fn f() -> impl Trait { /* ... body returning a single type ... */ }

The function writer chooses the concrete type returned, as opposed to generics/APIT, where the caller of the function can choose any type that meets the bounds. That's the point of RPIT, so there's no changing that.

Examples of where RPIT is parameterized by the input types are:

fn g<T: TraitOne>(t: T) -> impl TraitTwo {
    // returned type may depend on T
    // (but is otherwise concrete)
}

fn h(input: &str) -> impl Trait + '_ {
    // returned type may depend on lifetime of input
    // (but is otherwise concrete)
}

So then the question becomes, is it possible for "RPIT in APIT" to work like APIT? It seems very reasonable / straightforward for this example, but when you look at a parameterized case:

fn example(f: impl Fn(&str) -> impl Trait + '_) { /* ... */ }

This won't work:

fn example<F, R>(f: F)
where
    F: Fn(&str) -> R,
    R: Trait,

Because a type parameter like R can only represent a single type, where as the return type varies over lifetimes.

So you need something like

// pseudo-code
fn example<F>(f: F)
where
    F: Fn(&str) -> R<'_>,
    for<'any> R<'any>: Trait,

Where that R<'_> is a generic type constructor (which Rust doesn't have).

Or, for this particular case, you could have

// Using `Fn` traits in this form requires nightly features
fn example<F>(f: F)
where
    for<'any> F: Fn<(&'any str,)>,
    for<'any> <F as Fn<(&'any str,)>>::Output: Trait,

And you can emulate this on stable too with enough elbow grease... but

  • It tends to wreck inference
  • Rust currently can't deal with binders "changing levels" very well, like going from an inherently higher-ranked type fn(&str) -> SomeType<'_> to reasoning about a higher-ranked trait bound such as for<'any> <F as...>::Output: Bound where the higher-rank binder is "outside" the type

So can it happen? I think something that works like the last example can happen some day, but it's probably complicated to implement, and in my experience definitely needs a more sophisticated compiler in terms of higher-ranked bounds checking and inference.

If they allowed simple cases now, it might paint them into a corner with regards to how the fully general case is implemented.

3 Likes

It could, but the current behavior is almost always what one wants. A generic return type backed by a specific type would basically always be useless, because it can't possibly typecheck (generic arguments are chosen by the caller and can be arbitrary types).

I.e., if fn foo() -> impl ToString were desugared to

fn foo<R: ToString>() -> R {
   "this is a string"
}

then it wouldn't compile, because the generic parameter means that it could be called like this:

foo::<u32>();

Now obviously, a string literal is not a u32 but both implement ToString, hence the function body is meaningless when called with anything but &str as the R type parameter.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.