21

I have read several answers on SO already, and gathered these use-cases:

  • When a function panic!s
  • When a function has an infinite loop in it

But it is still unclear to me why we need to define the function like this:

fn func() -> ! {
    panic!("Error!");
}

if it will work the same way as this (without the exclamation sign):

fn func() {
    panic!("Error!");
}

and at the same time, why do we need to use ! in functions with infinite loops? It look like this signature doesn't bring any real usage information.

Oleksandr Novik
  • 489
  • 9
  • 24

2 Answers2

37

The main difference between these signatures boils down to the fact that ! can coerce into any other type, and thus is compatible with any other type (since this code path is never taken, we can assume it to be of any type we need). It's important when we have multiple possible code paths, such as if-else or match.

For example, consider the following (probably contrived, but hopefully clear enough) code:

fn assert_positive(v: i32) -> u32 {
    match v.try_into() {
        Ok(v) => v,
        Err(_) => func(),
    }
}

When func is declared to return !, this function compiles successfully. If we drop the return type, func will be declared as returning (), and the compilation breaks:

error[E0308]: `match` arms have incompatible types
 --> src/main.rs:8:19
  |
6 | /     match v.try_into() {
7 | |         Ok(v) => v,
  | |                  - this is found to be of type `u32`
8 | |         Err(_) => func(),
  | |                   ^^^^^^ expected `u32`, found `()`
9 | |     }
  | |_____- `match` arms have incompatible types

You can also compare this with definition for Result::unwrap:

pub fn unwrap(self) -> T {
    match self {
        Ok(t) => t,
        Err(e) => unwrap_failed("called `Result::unwrap()` on an `Err` value", &e),
    }
}

Here, unwrap_failed is returning !, so it unifies with whatever type is returned in Ok case.

Cerberus
  • 8,879
  • 1
  • 25
  • 40
  • 2
    It seems to me like `!` lets us to achieve something like dynamic typing. But at the same time, this doesn't work: `fn func() -> ! { 123 }`, although you said that __! can coerce into any other type, and thus is compatible with any other type__ – Oleksandr Novik Jan 22 '22 at 13:35
  • 11
    You're trying to do the opposite - coerce `i32` (the default integer type) into `!`. This obviously can't work. "Compatibility" here is one-directioned - "value" of type `!` (non-existant by definition) can be used wherever any other type is expected, not the other way around. – Cerberus Jan 22 '22 at 14:00
  • 5
    The reason it doesn't work like dynamic typing is that "!" is a promise that the function will NEVER return. Because of that, we can pretend it would have returned whatever type we want; this will never cause a problem since it never returns. That's why your example isn't allowed. Because it's not _actually_ dynamic typing. – Glenn Willen Jan 22 '22 at 22:34
  • 6
    @AleksandrNovik: This has nothing to do with dynamic typing. `!` is a *bottom type*, not a dynamic type, i.e. it is a type which is a subtype of every other type. – Jörg W Mittag Jan 23 '22 at 12:36
14

The compiler knows that anything that follows a diverging expression (w.r.t. evaluation order) is unreachable. It can use this information to avoid false negatives when it comes to deciding whether a local variable is initialized or not.

Consider the following example:

use rand; // 0.8.4

fn main() {
    let foo;
    if rand::random::<bool>() {
        foo = "Hello, world!";
    } else {
        diverge();
    }
    println!("{foo}");
}

fn diverge() {
    panic!("Crash!");
}

We declare a variable foo, but we only initialize it in one branch of the if expression. This fails to compile with the following error:

error[E0381]: borrow of possibly-uninitialized variable: `foo`
  --> src/main.rs:10:15
   |
10 |     println!("{foo}");
   |               ^^^^^ use of possibly-uninitialized `foo`

However, if we change the definition of our diverge function like this:

fn diverge() -> ! {
    panic!("Crash!");
}

then the code successfully compiles. The compiler knows that if the else branch is taken, it will never reach the println! because diverge() diverges. Therefore, it's not an error that the else branch doesn't initialize foo.

Francis Gagné
  • 60,274
  • 7
  • 180
  • 155