Enable linting for implicit returns

How do I enable linting for explicit returns? I hate them, and I don't understand why they're valid syntax in the first place. Is it really going to kill someone to type six characters and a space? They just seem like a needless way to make Rust code more confusing to read.

I understand that removing them from the language would break some things, so we unfortunately cannot purge the language of this nonsense, but I pray that I can at least make sure that such an insidious mechanism never finds its way into my codebase.

1 Like

What you think is an implicit "return" isn't a return at all, ie. it doesn't alter control flow.

Rust is an expression language, and blocks evaluate to their last expression. This is true for a function body too, that's all. What you think of as a special case in fact fits very well with the rest of the language.

That was completely unnecessary. If you don't understand the language well enough to be able to formulate an informed opinion about it, then better not start a rant about it. Have some humility and learn before whining.

20 Likes

#![deny(clippy::implicit_return)]

8 Likes

They're the same as how in Rust we idiomatically write

let x = if b { 7 } else { 13 };

rather than

let x;
if b {
    x = 7;
} else {
    x = 13;
}
13 Likes

I don't think I need to be an expert on Rust to have an opinion on it. It's widely known that Rust has its gatekeepers, but I find it kind of hilarious that they will go so far as to gatekeep the very act of having an opinion.

It's undeniable that implicit returns make code more confusing to read in many instances. On the other hand, no one has shown me where explicit returns make code prohibitively verbose. Do you have an example of some code that would be miserable to write if it required explicit return statements?

I'm happy to learn -- you just need to have something valuable to teach.

...for the people who haven't yet got an experience with it and haven't yet built an intuition for the whole "blocks are values" thing, which is used much more widely then just "implicit returns". For the ones who have, this "undeniable" sounds like at least an exaggeration.

16 Likes

It's unclear as to why we need those braces in the first place. An assignment requiring more than a single expression would seem like code smell to me.

Well put. This makes sense to me, and I can understand why implicit returns exist now. I still don't think they belong in a fn block, but now it's clear why it's valid syntax. I suppose this is where linters come in!

Fair enough -- go ahead and replace "It's undeniable that..." with "To the novice, it seems that..."

2 Likes

Those braces are necessary to disambiguate between the condition and the expression. The alternative would look something like let x = if b 7 else 15;, which looks way more confusing to me. It could get even worse:
let x = if b >= 5 && a != 0 c || d else false;
As opposed to
let x = if b >= 5 && a != 0 {c || d} else {false};

So really, I don't see how this could work without the braces.

10 Likes

Why would you think that? Most languages have something like this, though often they spell it differently -- x = b ? 7 : 13; in C, for example.

Rust is what it is, and some parts of it just aren't changing. Like people can have the opinion that languages are better with significant whitespace and no semicolons, but that's not an option that's useful to share in a Rust forum, since there's no way it's changing for Rust.

"Why doesn't this language A look like what I'm used to in language B? I hate it." isn't a productive discussion for any language pair.

If you want positive engagement, I suggest conveying curiosity and openness, and avoiding "hate", "kill someone", "needless", "nonsense", "insidious", etc.

19 Likes

Well that's also not entirely right.
There are a lot of expression-based languages that have "implicit return". From Lisp, to Ruby to Haskell, people coming from those languages will feel right at home with rust's expressionist nature.

14 Likes

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.

30 Likes

All good points, save for the first -- I don't think I've ever seen/heard someone speak positively of the ternary operator.

Both of those two additional examples contain smelly code in my opinion. I feel like there's gotta be a way to avoid the necessity for the original example of let x = if b { 7 } else { 13 }; in the first place. Besides that, those two additional examples make the case for explicit returns -- try the following:

let x = if b >= 5 && a != 0 return c || d else return false;

Again, though, I feel like it would be best to avoid the ternary operator or any syntactic sugar for it.

This code (after you add the missing braces) doesn't do what you think it does. The return expression exits the function. This would never assign anything to x.

12 Likes

Writing return <something>; at the end of a function in Rust is a bit like answering the question "Do you like potatoes?" with "Yes, I like potatoes" instead of simple "Yes".

8 Likes

This could be ambiguous to parse, too, considering closures:

let f = if a || b else || c;
// vs
let f = if a { || b } else { || c };
1 Like

I suspect you've mostly been using languages like C/C++, Java, C# etc i.e. non-expression-based languages that have a ternary operator.
In those languages the operator works, but is a bit like a fish out of water in terms of UX when compared to the rest of the language.

Rust is different from those languages in that Rust is expression-based to the bone.
Making if/else a statement rather than an expression would have been a major mistake given the expression-orientation of the language. And the real difference between what's called the ternary operator in C/C++, Java etc and the if/else expression in Rust is syntactic, not semantic.

If I may, perhaps it's your feelings standing in the way here. What if you put those aside for just a moment, and reflected on this:

Whether something, anything, is a code smell, is largely dependent on the semantics of the surrounding context, mainly the rest of the programming language. For example, this is a code smell in Java:

if (mystr == "hello") {
    //... 
}

But the direct equivalent code in Rust (i.e. same without the parens surrounding the predicate) is not. The reason for that comes down to how each language handles references. In Rust borrows are valid, period, while in Java any reference can be null, leading to either a longer, noisier predicate that's prefixed with a nullptr check, or Yoda conditions.

1 Like

It's also bad for an entirely different reason you seem to have missed: in Java, the == operator compares object addresses, while in Rust PartialEq recurses through references to compare values.

String constants may or may not be separate objects due to the vagaries of dynamic construction, interning, and multiple ClassLoaders. (Similarly in Rust with &'static str due to string deduplication, compilation units, and dynamic loading.)

3 Likes

While you're right about all of that, it wasn't important to my point, so I left it out.

3 Likes

We were speaking in hypotheticals... I made no claim as to the validity of my snippet. It was pseudocode for discussion purposes. It's worth noting that such a statement really isn't easy on the eyes any way you put it.

In cases where you really need a one-liner, I think Python does the best job by phrasing it as

x = c || d if b >= 5 && a != 0 else x = false

The confusing bit (that probably shouldn't be a one-liner) is now trapped between an if and an else with nowhere to hide and no way to make things confusing!