This certainly is an odd case. What's happening isn't directly because of ?
; it happens even when using a match
, such as
fn main() -> Result<(), ()> {
let _ = match parse() {
Ok(val) => val,
Err(_) => never(),
};
Ok(())
}
fn never() -> ! {
panic!();
}
It seems to be that if the compiler is required to unify !
(the “never” type, used to indicate the lack of a value, such as via divergence/panicking) and a type inference variable, it will choose ()
. The fact that this choice happens when the type inference variable would otherwise be a “cannot infer” type error if it weren't unified with !
is pretty clearly unintentional behavior, imho. On the other hand, though, this is also likely behavior we're stuck with to maintain backwards compatibility, at least until someone puts in the justification and effort to have different behavior in the next edition.
For fun, note that if we change to a trait which ()
doesn't implement, e.g.
fn parse<T: std::fmt::Display>() -> Result<T, ()> {
print_type_of::<T>();
Ok(todo!())
}
we get an error that ()
is not Display
error[E0277]: `()` doesn't implement `std::fmt::Display`
--> src/main.rs:11:19
|
11 | let _ = match parse() {
| ^^^^^ `()` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `()`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `parse`
--> src/main.rs:5:13
|
5 | fn parse<T: std::fmt::Display>() -> Result<T, ()> {
| ^^^^^^^^^^^^^^^^^ required by this bound in `parse`
due to type inference's choice of ()
here.
It might also have something to do with the ()
requirement for block-like expressions (e.g. match
) to be treated as a statement and elide the ;
. E.g. given
fn parse<T>() -> T {
print_type_of::<T>();
todo!()
}
fn main() {
if true {
parse()
} else {
never()
}
dbg!();
}
the code compiles with T=()
. parse();
results in the expected type error.
It's not just this, since ?
isn't considered a block-like expression, and match parse() { v => v }
causes a type error. But it's likely descended from this; an important case is that if all arms diverge, the block-like expression is still considered a statement, and the ;
isn't required. Before !
was a proper type, the way to do this would be to make the type collapse to ()
. With !
as a proper type, it would make more sense to unify the type as !
, but allow !
as well as ()
for expression statements.
?
is a really interesting special case, though. If defined as desugar like a macro, it's a match
with a !
-valued break arm and a T
-valued continue arm... but unifying to the !
type doesn't make any sense semantically. ?
would actually prefer that the inference of the continue type is completely unimpacted.
!
is just really odd in how it could be expected to behave. The behavior we have on stable is mostly just an organic evolution of what was convenient rather than anything planned.
Right now, I think I would say that unifying ?0
and !
because both are assigned to the same place should place no implications on ?0
, but assigning a value of type ?0
to a place of type !
should imply ?0 = !
. Assigning a value of type ?0
to a place of inferred type !
should probably change the place to type ?0
, unless the place's type is explicitly stated rather than inferred.
This all comes from !
's dual role: eventually, we want to be able to use it deliberately as a hole in generics, but it's also a "weak" type hole for divergence that should both decay to a real type but also not influence the choice of real types. It seems convenient to default to !
, e.g. Err(0i32)
is Result<!, i32>
, but there's a difference between an "explicit !
" and a "type inference !
," in that the former should influence further type inference (e.g. by unification with !
), but the latter probably shouldn't (e.g. a diverging arm making other arms infer to be !
-typed rather than a type error).
As a silly example, consider feature(exhaustive_patterns)
arm elision; given fn parse<T>() -> Result<T, !>
, match parse() { Ok(x) => x }
is perhaps surprisingly not a type error because we get T=()
. This is just clearly wrong IMHO, and T=!
more wrong; there's no !
type we're unifying with involved here at all, except for the deleted arm.