In Rust, the return
keyword is used for early returns, since it has the ability to exit the normal control flow of evaluation early.
In many other languages, this distinction is only made for cases where no values are returned.
For example in a for loop, one would usually not write
for i in some_collection() {
if foo(i) {
do_something();
else {
do_something_else();
}
let var = calc(i);
var.method().another_method();
continue;
}
instead, one would typically let the loop do an implicit continue;
at the end of the loop body. The continue;
statement would only be used for early-continue
, like in:
for i in some_collection() {
if foo(i) {
do_something();
continue;
}
let var = calc(i);
var.method().another_method();
}
As far as Iām aware, the same is typically true for return
in functions without a return value, too. Iāll still use Rust syntax, but think how youād do it in C (honestly, I never used C or C++ too much, so I wouldnāt know whatās actually idiomatic):
One wouldnāt write
fn foo(i: Foo) {
if foo(i) {
do_something();
else {
do_something_else();
}
let var = calc(i);
var.method().another_method();
return;
}
but instead
fn foo(i: Foo) {
if foo(i) {
do_something();
else {
do_something_else();
}
let var = calc(i);
var.method().another_method();
}
however, return;
is useful as an early return
as in
fn foo(i: Foo) {
if foo(i) {
do_something();
return;
}
let var = calc(i);
var.method().another_method();
}
Even with if
expressions at the end of a function (or a loop body), thereās an analogy, that one wouldnāt write
fn foo(i: Foo) {
if foo(i) {
do_something();
return;
} else {
do_something_else();
return;
}
}
and thereās an argument to be had whether
fn foo(i: Foo) {
if foo(i) {
do_something();
} else {
do_something_else();
}
}
or
fn foo(i: Foo) {
if foo(i) {
do_something();
return;
}
do_something_else();
}
is nicer (the latter can be better if in place of do_something_else()
) there is a long and/or nested piece of code, and/or the do_something_else()
case is some standard case whereas do_something
is exceptional, doing validation and early-returning an error, for instance.
Now with Rustās expression based syntax, we do have a way to transfer all these considerations to returns with values, too. return EXPR;
can serve the role of being used only in early returns, and using a trailing EXPR
(without semicolon) at the end of any block specifies a return value (of the block, which might thus also become the return value of the whole function).
The example code could thus, very analogously look as follows:
We donāt write
fn foo(i: Foo) {
if foo(i) {
do_something();
return fallback();
} else {
do_something_else();
return i;
}
}
but
fn foo(i: Foo) {
if foo(i) {
do_something();
fallback()
} else {
do_something_else();
i
}
}
and thereās an argument to be had, depending on the specific code at hand, whether
fn foo(i: Foo) {
if foo(i) {
do_something();
return fallback();
}
do_something_else();
i
}
might be preferred.
With these comparison explained in detail, Iād finally like to argue against your strawman āIs it really going to kill someone to type six characters and a space?ā
In my view, the main advantage of āimplicit returnsā(1) is not at all that one saves a few keystrokes, but instead that the āreturn
ā keyword thus becomes a keyword for explicit early returns, similar to how ācontinue
ā is a keyword for jumping early to the next iteration of a loop.
(1) which arguably are not actually implicit, but rather just āreturn
ā-keyword-less
This means that in idiomatic Rust codebases, the return
keyword, along with the ?
operator, serves as a well visible mark for more complicated control flow paths, and a lack or return
and ?
means at a glance that (ignoring unwinding) control flow is very structured and straightforward.