rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
99.13k stars 12.8k forks source link

Clarification of compiler diagnostic 'no two closures, even if identical, have the same type' #87961

Open GregAC opened 3 years ago

GregAC commented 3 years ago

Consider the following function which returns a closure impl

fn returns_closure(hmm: bool) -> impl Fn(i32) -> i32 {
    if hmm {
        |x| x + 1
    } else {
        |x| x * 2
    }
}

fn main() {
    println!("{}", returns_closure(true)(10));
    println!("{}", returns_closure(false)(10));
}

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

This builds and runs providing the following output:

11
20

This seems reasonable at first glance, however consider a changed version:

fn returns_closure(hmm: bool, y: u32) -> impl Fn(i32) -> i32 {
    if hmm {
        |x| x + y
    } else {
        |x| x * y
    }
}

fn main() {
    println!("{}", returns_closure(true, 3)(10));
    println!("{}", returns_closure(false, 4)(10));
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=43bdb248873d31bc470b0fc9fb8a2ab3

This fails to build with:

error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:5:9
  |
2 | /     if hmm {
3 | |         |x| x + y
  | |         --------- expected because of this
4 | |     } else {
5 | |         |x| x * y
  | |         ^^^^^^^^^ expected closure, found a different closure
6 | |     }
  | |_____- `if` and `else` have incompatible types
  |
  = note: expected type `[closure@src/main.rs:3:9: 3:18]`
          found closure `[closure@src/main.rs:5:9: 5:18]`
  = note: no two closures, even if identical, have the same type
  = help: consider boxing your closure and/or using it as a trait object

For more information about this error, try `rustc --explain E0308`.

The first example uses two closures and compiles and runs fine, demonstrating we do have two closures of the same type which the output from the second example states cannot happen.

I suspect what's happening is the first closures don't capture from the environment so are seen as function pointers both with type fn(i32) -> i32. So this is just an issue of clarifying the second error message e.g. 'no two closure which capture the environment, even if identical, have the same type'.

Changing the first example as follows seems to confirm this:

fn returns_closure(hmm: bool) -> fn(i32) -> i32 {
    if hmm {
        |x| x + 1
    } else {
        |x| x * 2
    }
}

fn main() {
    println!("{}", returns_closure(true)(10));
    println!("{}", returns_closure(false)(10));
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c628f6a790b886b7975542a7fd89f734

(Builds and runs with identical results to the first example)

From reading the rust reference you could say 'closure' inherently means environment capture (i.e. |x| x + 1 is not a closure), but that conflicts with language used in the rust book so could lead to confusion.

If it's intended that closures from the first example should have different types there's something more serious going on with the type inference/checking.

rustc --version --verbose:

rustc 1.56.0-nightly (ccffcafd5 2021-08-11)
binary: rustc
commit-hash: ccffcafd55e58f769d4b0efc0064bf65e76998e4
commit-date: 2021-08-11
host: x86_64-unknown-linux-gnu
release: 1.56.0-nightly
LLVM version: 12.0.1
thomcc commented 3 years ago

I suspect what's happening is the first closures don't capture from the environment so are seen as function pointers both with type fn(i32) -> i32. So this is just an issue of clarifying the second error message e.g. 'no two closure which capture the environment, even if identical, have the same type'.

I think your analysis here is mostly right. The thing is it's actually true that no two closures have the same type, even if they don't capture their environment. What's happening is that closure-to-fn coersion is taking place in the first example — they do have different types, but are coerced to a common type.

So, I'm not sure how to fix the diagnostic, since it's not wrong, it just looks wrong whereas your suggestion is more wrong, even though it appears to match the behavior... The behavior is just unintuitive...

(Sadly, I don't think the rules by which this happens are very well specified — I believe it's less specified than Deref or Unsizing coersion, but maybe I'm just unaware of were the docs are).

GregAC commented 3 years ago

I guess it depends how verbose you allow the diagnostics to become an extra

note: two closures can be coerced to the same fn type under certain circumstances, this was not possible here

or similar could work?

jyn514 commented 3 years ago

I think it would be useful to suggest coercing both closures to a function pointer if possible, but I don't think the diagnostic needs to mention function pointers if coercing isn't possible. Diagnostics should only give you as much info as you need to fix the problem.

camsteffen commented 2 years ago

I recently learned this is possible and I also wish rustc had taught me this sooner. I think it should say something like:

help: consider using inputs instead of captured variables

 |        |x, y| x + y
            +++
hardfau1t commented 1 year ago

Similar to above

fn main() {
    let haa = if true {
        Some(|x:u32| x + 1)
    } else {
        Some(|x:u32|  x + 2)
    };
}

this throws error as if and else have incompatible types. But this compiles fine

fn main() {
    let haa = if true { 
        |x: u32| x + 1 
    } else {
        |x: u32| x + 2 
    };
}