Type-checking ! vs i32

This doesn't compile, but it seems like it should:

fn panic() -> ! { panic!("foo") }
fn f(x: Option<i32>) -> i32 { x.unwrap_or_else(panic) }
  |
2 | fn f(x: Option<i32>) -> i32 { x.unwrap_or_else(panic) }
  |                                 ^^^^^^^^^^^^^^ expected `!`, found `i32`
  |
  = note: expected type `!`
             found type `i32`

Shouldn't FnOnce() -> ! match FnOnce() -> i32?

1 Like

No, it shouldn't. They are different traits, and the never type only let's you coerce between different types

You can do something like the following if y9u want it to compile

fn panic<T>() -> T { panic!("foo") }
fn f(x: Option<i32>) -> i32 { x.unwrap_or_else(panic) }
1 Like

This also works:

fn panic() -> ! { panic!("foo") }
fn f(x: Option<i32>) -> i32 { x.unwrap_or_else(|| panic()) }

The original code doesn't compile because type coercion only happens on the top level, not in the type parameters. For example, &String coerces to &str, but Option<&String> doesn't coerce to Option<&str>, and Fn(&String) doesn't coerce to Fn(&str).

1 Like

I still don't really understand. According to [Subtyping and Variance - The Rust Reference]:

fn()->T is covariant in T, which as defined there implies that if T is a subtype of U, then fn()->T is a subtype of fn()->U.

! is a subtype of i32, which should imply that the function panic, being of type fn()->!, should also be considered to be (a subtype) of type fn()->i32.

panic is not of type fn() -> !. It has an anonymous type, which happens to implement Fn() -> ! (note the capitalization). It does not implement Fn() -> i32, which is the trait requirement of unwrap_or_else. panic can be coerced to fn() -> !, but even if you do that it will still not implement Fn() -> i32 because they are different traits.

Moreover, ! is also not a subtype of i32. ! is coercible to i32, but that does not make a subtype relationship (previous discussion about this topic). For essentially the same reason, &String is coercible to &str, but &&String is not coercible to &&str.

Subtyping in Rust has to do with lifetimes. fn() -> &'static str is a subtype of fn() -> &'a str (actually that last one is hard to express, but you get the point). No lifetimes = no subtyping.

6 Likes

No, subtyping only applies to lifetimes. All other subtyping-like relations are merely conversions that the language support, and they are not subject to covariance.

3 Likes