Enforces the explicit 'return' keyword when Function body appears 'return'

fn foo() -> i32 {
    0 // it is good, so beautiful
}
fn bar() -> i32 {
    if false {
        // There's a return
        return 0;
    } else {
    }
    //should force an explicit 'return' here
    1
}
  1. Make the code consistent, easy to read, and more beautiful
  2. There's no harm in doing it.
  3. The other option is simply display 'return' in the lsp

That’s something I’m pretty sure a majority of Rust programmers would disagree with. At least I myself find the lack of return more consistent here, I’ve written a very lengthy response before on why that’s the case over in the users forum. TL;DR: In Rust, “return” keyword means “early return”. Your if expression features an early return, marked with the return keyword. The end of the function give a return value in the usual return position. You don’t put a continue at the end of your loops[1] just because there’s some continue (for early continuing to the next iteration) in your loop or while loops either.

Of course there’s harm in turning programs that compiler fine now into errors. Or what does “enforce” mean for you?

Sure, that’s maybe not entirely unreasonable; as an optional feature.


  1. We could quite literally make the same argument there, by which I mean, I personally would disagree with this argument in either case:

    fn foo(mut n: u32) {
        while n > 0 {
            println!("We have come across the number {n}");
            n -= 1;
        }
    }
    fn bar(mut n: u32) {
        while n > 0 {
            if n == 42 {
                n -= 1;
                continue; // There's a continue
            }
            println!("I don't like the number {n}");
            n -= 1;
            continue; //should force an explicit 'continue' here
        }
    }
    
    ↩︎
24 Likes

If you really want to enforce this, there's a clippy lint for it:

clippy::implicit_return

9 Likes

Frequently Reqested Changes: Fundamental Changes to Rust Syntax

Changes that would break existing Rust code are non-starters. Even in an edition, changes this fundamental remain extremely unlikely. The established Rust community with knowledge of existing Rust syntax has a great deal of value, and to be considered, a syntax change proposal would have to be not just better, but so wildly better as to overcome the massive downside of switching.

9 Likes

Thank you for your example. You compared 'return' to 'continue' to create a different intuition than mine, and it looks like your intuition is pretty good, too. But if 'return' means 'early return',rust should prohibit 'return' from appearing on the last line. No one can deny that the use of 'return' is confusing in rust, This has to do with its compatibility with expressions and statements. my idea is that 'return' is the language of statements, and if it appears in the body of a function, the function should be treated as a statement rather than an expression. So this function should return from 'return'.

Well it certainly isn't confusing to anybody coming from another expression-based language, ie Ruby, ML family, Haskell, various lisp flavours.

2 Likes

I see it being confusing to new users, who assume else { value } is always the same as else { return value; }, due to this functionality being taught on small single-statement functions where this happens to be true.

However, I haven't seen not-novice users confused about returns. It seems that once people reframe this correctly as blocks having a value, they're able to use it properly. And there's a clippy lint that helps keep code consistent with what Rust considers consistent: always using the implicit value where possible.

11 Likes

Yeah, personally, when dealing with beginners I try to use "evaluates to" and "is" rather than "returns". This is a much less confusing way for people to think about it in the long run. Generally, analogies between Rust and other languages tend to break down pretty quickly and become an incorrect way to reason about code.

5 Likes

The clippy::needless_return lint covers this, and is warn-by-default.

5 Likes

I see, thanks for your reply.

My personal preference is to use implicit return when the function is small and simple and explicit return otherwise. "Small and simple" is of course hard to define and therefore hard to enforce algorithmically.

What you see sometimes in the wild are 100 lines long match statements, where some of the arms have nested conditions and you need to know a lot of context to figure out that an expression with 4 nesting levels is actually the returned value.

Just to share another perspective which ties into the "evaluates to" -explanation, you can also view returning as a special case of breaking from a labelled block: Evaluating break 'foo x means that "the containing block with label 'foo evaluates to x", and return x is just the same but referring to the innermost containing function body. So these are essentially the same:

fn foo() -> R {
    //...
    if cond() { return val; }
    //...
}
fn bar() -> R { 'func: {
    //...
    if cond() { break 'func val; }
    //...
}}

However, since "breaking from the function" is vastly more common than breaking from any other block, and a familiar feature from many other languages, it makes a lot of sense to have that special case as its own construct. (And to be clear, that's just looking at the language as it stands today. return was there from the start of course, while labelled blocks only got to stable in Rust 1.65 )

As an interesting "alternative" to the clippy lint, you can locally enforce "no implicit return" by wrapping the body in a block that must not evaluate to any value, for example:

fn bar() -> i32 {
    match { 
        /* function body here */
    } { /* match has no arms */  }
}

The function body can return from bar, but the inner block cannot evaluate to anything, because then the match wouldn't be exhaustive. (I'm sharing this merely as a "look at all the cool things rustc can do", please don't actually write code like that!)

4 Likes