Normally, every branch in the if
expression should have the same type. If the type for some branch is underspecified, the compiler tries to find the single common type:
fn print_number(x: int, y: int) {
let v = if x + y > 20 {
3 // this can be either 3u, 3i, 3u8 etc.
} else {
x + y // this is always int
};
println!("{}", v);
}
In this code, 3
is underspecified but the else
branch forces it to have the type of int
.
This sounds simple: There is a function that "unifies" two or more types into the common type, or it will give you an error when that's not possible. But what if there were a fail!
in the branch?
fn print_number(x: int, y: int) {
let v = if x + y > 20 {
fail!("x + y too large") // ???
} else {
x + y // this is always int
};
println!("{}", v); // uh wait, what's the type of `v`?
}
I'd want that fail!
does not affect other branches, it is an exceptional case after all. Since this pattern is quite common in Rust, the concept of diverging type has been introduced. There is no value which type is diverging. (It is also called an "uninhabited type" or "void type" depending on the context. Not to be confused with the "unit type" which has a single value of ()
.) Since the diverging type is naturally a subset of any other types, the compiler conclude that v
's type is just that of the else
branch, int
.
Return
expression is no different from fail!
for the purpose of type checking. It abruptly escapes from the current flow of execution just like fail!
(but does not terminate the task, thankfully). Still, the diverging type does not propagate to the next statement:
fn print_number(x: int, y: int) {
let v = if x + y > 20 {
return; // this is diverging
() // this is implied, even when you omit it
} else {
x + y // this is always int
};
println!("{}", v); // again, what's the type of `v`?
}
Note that the sole semicoloned statement x;
is equivalent to the expression x; ()
. Normally a; b
has the same type as b
, so it would be quite strange that x; ()
has a type of ()
only when x
is not diverging, and it diverges when x
does diverge. That's why your original code didn't work.
It is tempting to add a special case like that:
- Why don't you make
x; ()
diverging when x
diverges?
- Why don't you assume
uint
for every underspecified integer literal when its type cannot be inferred? (Note: this was the case in the past.)
- Why don't you automatically find the common supertrait when unifying multiple trait objects?
The truth is that, designing the type system is not very hard, but verifying it is much harder and we want to ensure that Rust's type system is future-proof and long standing. Some of them may happen if it really is useful and it is proved "correct" for our purpose, but not immediately.