Why is using return as the last statement in a function considered bad style?
Asked Answered
I

6

74

I was reading through the Rust documentation and came across the following example and statement

Using a return as the last line of a function works, but is considered poor style:

fn foo(x: i32) -> i32 {
    if x < 5 { return x; }

    return x + 1;
}

I know I could have written the above as

fn foo(x: i32) -> i32 {
    if x < 5 { return x; }

    x + 1
}

but I am more tempted to write the former, as that is more intuitive. I do understand that the function return value should be used as an expression so the later works but then why wouldn't the former be encouraged?

Injury answered 15/1, 2015 at 10:58 Comment(2)
IMO it's a poor choice and hurts clarity and searchability. I won't be surprised if this style recommendation is reverted in the future. It's great in a single-expression closure, but not in a multi-statement function, and I find it especially awkward when there are multiple return paths...Jamboree
the recommendation is only for a return as the last statement. If you have multiple return paths, you don't have a choice but to use returnSerrulate
F
38

Copied from reddit: Why isn't the syntax of return statements explicit?


Answer from @pcwalton

Explicit return is really annoying in closures. For example, it was a major pain in JavaScript before ES6 arrow functions were introduced

myArray.map(function(x) { return x * 2; })

is gratuitously verbose, even without the function keyword. Once you have implicit returns somewhere in your language, you might as well have them everywhere for consistency's sake. The fact that it makes code less verbose is just an added bonus.

and from @mozilla_kmc

Rust is an expression-oriented language. A block has the form

{
    stmt;
    stmt;
    ...
    stmt;
    expr
}

The statements are (basically) expressions or let bindings, and the trailing expression is implicitly () if not specified. The value of the whole block is the value of this last expression.

This is not just for functions. You can write

let foo = if x { y } else { z };

so if also takes the place of C's ?: operator. Every kind of block works the same way:

let result = unsafe {
    let y = mem::transmute(x);
    y.frob()
};

So the implicit return at the end of a function is a natural consequence of Rust's expression-oriented syntax. The improved ergonomics are just a nice bonus :)

Puzzle: return x itself is an expression -- what is its value?

Answer (suggested by @dubiousjim):

It is a never type !.

False answered 30/8, 2018 at 16:11 Comment(1)
The type of return x is whatever the context needs. If it's in one arm of an if and the other arm is of a non-unit type, then so too will return x have that type. So you could think of it as having bottom type (a subtype of all other types) and thus no value (since bottom type has no instances). Or with more sophistication, you could give it a continuation type and value. It's not right to say it has unit type and value.Adlee
D
18

It just is.

Conventions don’t need to have particularly good reasons, they just need to be generally accepted conventions. As it happens, this one does have a comparatively good reason—it’s shorter as you don’t have the return and ;. You may think that return x + 1; is more intuitive, but I disagree strongly—it really grates and I feel a compelling need to fix it. I say this as one who, before starting using Rust, had never used an expression-oriented language before. While writing Python, return x + 1 in that place looks right, while writing Rust it looks wrong.

Now as it happens, that code should probably be written thus instead:

fn foo(x: i32) -> i32 {
    if x < 5 {
        x
    } else {
        x + 1
    }
}

This emphasises the expression orientation of the language.

Demeter answered 15/1, 2015 at 11:10 Comment(7)
as said document-or, I'd agree that your version is how i'd write this for real too. pull request time...Allaround
Whenever there's an else, I would rather see match used; it's much nicer.Foreboding
@Tshepang: what, match x { true => …, false => … }? I definitely disagree with you there.Demeter
I prefer return in those positions, actually. Because Rust is expression-oriented, it becomes a bit more difficult to track which blocks are exited enough times that the whole function returns, and it becomes awkward to see a return stmt or two in the middle when you need to return early... Expression-orientation has so many other style-advantages already but this feels like a _dis_advantage to follow, conversely... (for longer functions, at least)Deflexed
"It just is." is not a good answer for adults. Conventions and styles are in the eyes of the users. If the majority of users find using return intuitive, then it is a good style, regardless what the language designers' opinions. [comments too long, read the rest following the link below] gist.github.com/hzhou/432581d0735c035993a1d4bb1863d367Teller
I think this is the correct answer in sense that it's just a convention. I don't personally agree with that and I prefer explicit return because the return statement is special and my code doesn't depend on a single semicolon anymore. I can see some value for the option to use implicit return syntax / expression syntax instead of a statement in e.g. lambda (/anonymous) functions, but I don't consider that superior as a general rule. It's a bit silly that clippy emits a warning for breaking this convention but is perfectly happy to accept Allman-8 style which is also against the convention.Mehta
"It just is". In what sense? Who defined the conventional styles? As a person coming from Python, always writing return is something I feel comfortable with, so that should be my style, and should not be considered "bad".Livesay
L
8

The clippy lint gives the following rational for the needless_return lint:

Removing the return and semicolon will make the code more rusty.

This is probably as good as an objective rationale as we will ever get.

As far as intuition goes; I feel that it is shaped by our individual experiences and therefore subjective. While Rust is not a functional programming language itself, many people using and developing it seem to have a strong background in functional programming languages like Haskell, which are entirely based on expressions. The influence can be strongly felt in many areas of Rust (e.g. error handling). So for them (and to be honest, myself included) using an expression instead of a statement seems more elegant.

Lanthorn answered 30/8, 2018 at 18:9 Comment(0)
L
7

I strongly suspect it is derived from the functional programming style. (As per @Markus Klein's answer).

Your example, in OCaml:

let foo x = if x < 5 then x else x + 1

Compare to your example, modified, in Rust:

fn foo(x: i32) -> i32 {
    if x < 5 { x } else { x + 1 }
}

Example of a closure, in OCaml:

fun x -> if x < 5 then x else x + 1

Compare to a Rust closure:

|x| if x < 5 { x } else { x + 1 }

So Rust seems to adopt the functional programming style.

Why do functional programming languages do this? I'm not sure. Probably because throwing return's around everywhere would be annoying and pretty pointless when the meaning is already clear from the code.

Functional programming languages don't really rely on the classic "assign to variable, operate on variable, return variable" paradigm and instead use the "assign to variable, pipe variable through several different functions, the final result is the output" paradigm (and even assigning to a variable is optional: search up "Point-free programming").

"Classic" paradigm example in Rust:

fn qux(mut x: i32) -> i32 {
x = foo(x);
x = bar(x);
return x; // classic C style
}

Versus functional programming paradigm in OCaml:

let qux x = x |> foo |> bar

If we demanded a return in the OCaml example we would need to put it at the very beginning, which is largely pointless since we already know what it's going to return.

So probably this functional programming style carries over to Rust for the reasons stated above.

Example:

Rust's if statements evaluate to an expression, unlike C ("Classic") and like OCaml (Functional).

Lanam answered 30/5, 2021 at 19:55 Comment(0)
M
2

I am perfectly happy with the following solution: configuring clippy to NOT issue a warning for an unnecessary return AND warning me when a final return is missing.

For that it is sufficient to add at the top of the root file: the ''main.rs'' or the ''lib.rs'' the following clippy lint:

#![deny(clippy::implicit_return)]
#![allow(clippy::needless_return)]

The added bonus for us is, that in some closure, it prevents curly braces to be removed by rustfmt. In my team it is unanimously considered better for clarity to have curly braces in closure, adding extra separation between function parameters and code. In the following example:

#[derive(Clone)]
struct User {
    username: String,
}

impl User {
    pub fn new(username: String) -> User {
        return User { username };
    }

    // Code before the pass of rustfmt
    pub fn get_in(&self, users: Vec<User>) -> Option<User> {
        users.into_iter().find(|user| {
            user.username == self.username
        })
    }
}

before the added lint, rustfm would transform the get_in method into:

// Code after the pass of rustfmt
pub fn get_in(&self, users: Vec<User>) -> Option<User> {
   users.into_iter().find(|user| user.username == self.username)
}

Now, we get:

pub fn get_in(&self, users: Vec<User>) -> Option<User> {
       return users.into_iter().find(|user| {
           return user.username == self.username;
       });
    }

It is certainly a matter of taste, but we are unanimous in my team to prefer the latter, especially because the presence or absence of a single semicolon (hard to see) is not the cause of a type mismatch. Thus, we are very happy with that solution (even with the verbosity added by the 2 return statements). Ultimately, we would need another lint or rustfm option to handle our special taste in matter of curly braces ...

Magneton answered 25/1, 2023 at 11:26 Comment(0)
P
0

Unlike popular OOP languages, Rust is EOP, where EOP = expression oriented programing

When endding with ;, that is an expression, thus block is an expression.

On the contrary, when endding without ;, that is a statement.

We write a simple function to add one,

fn add_one(x: i32)  {
    x + 1;
}

fn main() {
    println!("{:#?}", add_one(1));
}

./target/debug/mytest
()

Let us remove ;,

fn add_one(x: i32)  {
    x + 1
}
    
fn main() {
    println!("{:#?}", add_one(1));
}

./target/debug/mytest //can't do this

This can't be compiled, since function mismatches type tuple to i32

We modify the function,

fn add_one(x: i32) -> i32 {
    x + 1
}

fn main() {
    println!("{:#?}", add_one(1));
}

./target/debug/mytest
2

Someone says this is implicit return, yet I treat -> as return.

Besides, we can force type into i32 by adding return,

fn add_one(x: i32) -> i32 {
    return x + 1; //without return, type mismatched
}

fn main() {
    println!("{:#?}", add_one(1));
}

./target/debug/mytest
2

Now we make an option to choose,

fn add_one(x: i32, y: i32, s: i32) -> i32 {
    if s == 1 {
        x + 1
    } else if s == 2 {
        y + 1
    } else {
        0
    }
}

fn main() {
    println!("{:#?}", add_one(1, 2, 1));// choose x, not y
}

In terms of Bad Style, that is mostly in these condition flow, especially when it's getting large...

Another case would be trinary assignment,

fn add_one(x: i32, y: i32, s: i32) -> i32 {
    if s == 1 { x + 1 } else { y + 1 }
}

fn main() {
    println!("{:#?}", add_one(1, 2, 1));
}

More meaningfully in bool,

fn assign_value(s: bool) -> i32 {
    if s { 5 } else { -5 }
}

fn main() {
    println!("{:#?}", assign_value(false));
}

./target/debug/mytest
-5

In summary, we'd better not using return, instead we clarify the type for function

Predestinarian answered 8/6, 2021 at 3:16 Comment(1)
The explanation of what you get when using or not using a semicolon is exactly the opposite from what is true. Reads the Rust documentation: Expressions do not include ending semicolons. If you add a semicolon to the end of an expression, you turn it into a statement, which will then not return a value.Underlying

© 2022 - 2024 — McMap. All rights reserved.