Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

error where expected and found future are the same #112010

Closed
rbtcollins opened this issue May 26, 2023 · 4 comments
Closed

error where expected and found future are the same #112010

rbtcollins opened this issue May 26, 2023 · 4 comments
Labels
A-async-await Area: Async & Await A-diagnostics Area: Messages for errors, warnings, and lints C-bug Category: This is a bug.

Comments

@rbtcollins
Copy link
Contributor

rbtcollins commented May 26, 2023

I tried this code:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=5c0da276115b23a4478fd825fb91f21a

(I'm trying to get some way to port the rustup test suite which uses closures to run test within the context of the framework over to async code)

I got this error:

23 |     call_a_callback(make_future);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected trait `for<'a> <for<'a> fn(&'a str) -> impl Future<Output = ()> + 'a {make_future} as FnOnce<(&'a str,)>>`
              found trait `for<'a> <for<'a> fn(&'a str) -> impl Future<Output = ()> + 'a {make_future} as FnOnce<(&'a str,)>>`
note: the lifetime requirement is introduced here
  --> src/main.rs:5:20
   |
5  |     F: Fn(&str) -> FR,
   |                    ^^

On

Nightly version: 1.71.0-nightly
(2023-05-25 a2b1646c597329d0a25e

The following error

16 | fn make_future<'a>(s: &'a str) -> impl Future<Output=()>+ 'a {
   |                                   --------------------------
   |                                   |
   |                                   the expected future
   |                                   the found future
...
23 |     call_a_callback(make_future);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected opaque type `impl for<'a> Future<Output = ()> + '_`
              found opaque type `impl Future<Output = ()> + '_`
   = help: consider `await`ing on both `Future`s
   = note: distinct uses of `impl Trait` result in different opaque types

Meta

rustc --version --verbose: (from playground, not sure how to get --verbose there)

1.69.0
<version>

I'm not entirely sure what is going on.

What I hoped would happen is that the future returned from make_future would be suitable for awaiting on in call_a_callback - the bigger picture is I'm trying to port rustups test suite to async, and this is a minimal reproduction of the code style there, where async lifetime errors are being a nuisance ;)

@rbtcollins rbtcollins added the C-bug Category: This is a bug. label May 26, 2023
@jyn514 jyn514 added A-diagnostics Area: Messages for errors, warnings, and lints A-async-await Area: Async & Await labels May 26, 2023
@jyn514
Copy link
Member

jyn514 commented May 26, 2023

@lukas-code
Copy link
Member

lukas-code commented May 26, 2023

Slightly smaller version without futures: playground

fn call_a_callback<F, FR>(_: F)
where
    F: Fn(&str) -> FR,
{}

fn make_future(_: &str) -> impl FnOnce() + '_ {
    || ()
}

fn main() {
    call_a_callback(make_future);
}
error[[E0308]](https://doc.rust-lang.org/nightly/error_codes/E0308.html): mismatched types
  --> src/main.rs:11:5
   |
6  | fn make_future(_: &str) -> impl FnOnce() + '_ {
   |                            ------------------
   |                            |
   |                            the expected opaque type
   |                            the found opaque type
...
11 |     call_a_callback(make_future);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected opaque type `impl for<'a> FnOnce() + '_`
              found opaque type `impl FnOnce() + '_`
   = note: distinct uses of `impl Trait` result in different opaque types
note: the lifetime requirement is introduced here
  --> src/main.rs:3:20
   |
3  |     F: Fn(&str) -> FR,
   |                    ^^

related: #102400

Why this happens

To call the function call_a_callback, the compiler has to fill in the generic parameters F and FR with some concrete types.

The desugaring of the function item looks something like this:

/// Type of the fn item `make_future`. Note no generics.
struct MakeFuture;
impl<'a> Fn<&'a str> for MakeFuture {
    type Output = MakeFutureReturn<'a>;
    fn call(_: &'a str) -> MakeFutureReturn<'a> { /* ... */ }
}

/// "Opaque" type of the return value of `make_future`.
/// Generic over the lifetime of the fn arg of `make_future`.
struct MakeFutureReturn<'a> { /* ... capture the `&'a str` maybe ... */ }
impl<'a> Future for MakeFutureReturn<'a> { /* ... */ }

So the function call with the generic parameters filled in becomes:

fn main() {
    // Note that the compiler has to choose a concrete lifetime 'b here.
    call_a_callback::<MakeFuture, MakeFutureReturn<'b>>(make_future);
}

Next, the compiler has to verify the F: Fn(&str) -> FR bound, which actually means F: for<'a> Fn(&'a str) -> FR. So with F and FR filled in, this becomes MakeFuture: for<'a> Fn(&'a str) -> MakeFutureReturn<'b> with the concrete lifetime 'b that was chosen above. This is not statisfied, because MakeFuture implements for<'a> Fn(&'a str) -> MakeFutureReturn<'a> with the lifetime 'a from the function argument.

Maybe the diagnostic could ouput something like this?

note: this type parameter is inferred to be `impl Future<Output = ()> + '1` for some specific lifetime `'1` ...
  --> src/main.rs:1:27
   |
1  |     fn call_a_callback<F, FR>(_: F)
   |                           ^^
note: ... but the type returned by the the function is `impl Future<Output = ()> + '2` where `'2` is a lifetime from a function argument.
  --> src/main.rs:6:49
   |
6  |     fn make_future(_: &str) -> impl FnOnce() + '_ {
   |                       ^                        ^^

How to make it work

pile of bad code, unrelated to the diagnostics issue

To make the error go away, you have to prevent the compiler from choosing the wrong lifetime for FR. A simple way to achieve this is to get rid of the generic entirely and replace it with a boxed future: playground

use std::pin::Pin;
use std::future::Future;

async fn call_a_callback<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> Box<dyn Future<Output = ()> + 'a>
{
    let s = "hello";
    Pin::from(f(&s)).await
}

async fn print_s(s: &str) {
    println!("got {s}");
}

fn make_future<'a>(s: &'a str) -> Box<dyn Future<Output=()> + 'a> {
    Box::new(print_s(s))
}

#[tokio::main]
async fn main() {
    call_a_callback(|s| Box::new(print_s(s))).await;
    call_a_callback(make_future).await;
    call_a_callback(|s| Box::new(async move {
        println!("got {s}");
    })).await;
}

But muh zero-cost abstractions?!?

If you really want to make this work without boxing, this can be done with a combination of generic associated types (GAT) and type alias impl trait (TAIT) on the nightly compiler: playground

#![feature(type_alias_impl_trait)]

use std::future::Future;

trait FutureFamily<Output> {
    type Future<'a>: Future<Output = Output>;
}

async fn call_a_callback<F, FR: FutureFamily<()>>(f: F)
where
    F: for<'a> Fn(&'a str) -> FR::Future<'a>,
{
    let s = "hello";
    f(&s).await
}

async fn print_s(s: &str) {
    println!("got {s}");
}

type NamedFuture<'a> = impl Future<Output=()> + 'a;

struct NamedFutureFamily;
impl FutureFamily<()> for NamedFutureFamily {
    type Future<'a> = NamedFuture<'a>;
}

fn make_future<'a>(s: &'a str) -> NamedFuture<'a> {
    print_s(s)
}

#[tokio::main]
async fn main() {
    call_a_callback::<_, NamedFutureFamily>(make_future).await;
}

This is quite ugly, but works, because now the generic FR is no longer filled in with a concrete lifetime, but essentially a "type constructor" that can create the future's type with any lifetime. There might be a better solution, but I can't think of one right now.

@rbtcollins
Copy link
Contributor Author

Thank you so much for diving deep on my report!

@eholk
Copy link
Contributor

eholk commented Sep 25, 2023

This looks like the same issue as #74497, so I'll resolve it in favor of that one. The main thing to fix here is some kind of improve error message.

@eholk eholk closed this as completed Sep 25, 2023
@fmease fmease closed this as not planned Won't fix, can't repro, duplicate, stale Jan 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-async-await Area: Async & Await A-diagnostics Area: Messages for errors, warnings, and lints C-bug Category: This is a bug.
Projects
None yet
Development

No branches or pull requests

5 participants