This only seems to become a problem because of a few exceptional quirks around the unit type ()
:
()
is the default when the return type is omitted from a function signature (so fn main()
is equivalent to fn main() -> ()
);
- And even if you do not provide any expression to return, empty blocks or statements in code also evaluate to
()
.
The example below works because the semi-colon turns the expression 5
into a statement, the value of which is hence discarded.
fn foo() {
5;
}
Recursively, it is easy for all match arms to evaluate to ()
when none of them yield an outcome of another type. This is the case when using return
because the return statement creates a true divergence from the execution flow: it evaluates to the never type !
, which coerces to any other type.
fn foo(bar: i32) {
match bar {
1 => {
return do_good_things(); // coerces to () because of the default match arm
}
0 => {
return do_other_things(); // coerces to () because of the default match arm
}
_ => {
// arm evaluates to (), oops
}
}
}
The ubiquity of this unit type generally contributes to elegant code. In this case however, it may trigger a false positive when a more strict control flow is intended. There is no way for the compiler to resolve this unless we introduce another type to counter it.
Therefore, these solutions are possible:
- Use a different return type for the function. If there is nothing applicable to return (e.g. only side-effects), you can use nearly any type, but another unit type provides a better assurance that it becomes a zero-cost abstraction.
Playground
struct Check;
fn foo(bar: i32) -> Check {
match bar {
1 => {
do_good_things();
Check
}
0 => {
do_other_things();
return Check; // can use return
}
_ => {
// error[E0308]: expected struct Check, found ()
}
}
}
- Do not use
return
or break
statements, and establish that all of your match arms need to evaluate to something other than ()
.
Playground
struct Check;
fn foo(bar: i32) {
let _: Check = match bar {
1 => {
do_good_things();
Check
}
0 => {
do_other_things();
Check
}
_ => {
// error[E0308]: expected struct Check, found ()
}
};
}
- The reverse: establish that the match expression evaluates to a zero type (one like the never type
!
), so that no match arm can return from it except using control flow statements such as break
or return
.
Playground
enum Nope {}
fn foo(bar: i32) {
let _: Nope = match bar {
1 => {
return do_good_things();
}
0 => {
return do_other_things();
}
_ => {
// error[E0308]: expected enum `Nope`, found ()
}
};
}
See also: