Enable linting for implicit returns

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.

32 Likes