Implicit vs Explicit

It is better to use
return <whatever>;
than
<whatever>
.

Prove me wrong.

grep '^\s*return .*;' blahblah.rs

Implicit returns are part of the expression-oriented part of the syntax. It is needed for structures like if expressions, match expressions with code blocks that give a value, and many others. When you're used to using those, it leads to a different mental model of your code where leaving out "return" feels natural as well. It's not unique to rust either, so it doesn't use much of the "strangeness" budget. I think the only argument against it is that people coming from other languages haven't seen it before, but that's not a particularly strong argument given how natural it is once you get used to it.

I don't think I've ever needed to grep a return statement before.

11 Likes

As you seem to like strong introductory statements, I don't think there is such a thing as "implicit return".

What does ist mean? It suggests there are "true returns" that are hidden but added by the compiler somehow. How many returns are added in the following code:

fn max(x : i32, y : i32) -> i32 {
    if x > y { x } else { y }
}

Is it one? Maybe it's two? Or three? What would grepping returns help with analysing this function? And where should one put returns for this?

2 Likes

When a function runs off the end of the list of statements it contains there is only one thing that can happen, control flow returns to the site of the function call. That is what functions are all about. So clearly return is redundant in that position.

I could make an appeal to consistency and point out that natural flow execution of loops is cut short by continue and break. Similarly the natural flow of execution of functions is prematurely cut short by return. So in the same way return is used only when it is needed.

5 Likes

21 Likes

:exploding_head:

1 Like

Why does this even work?
Is the "return X;" statement actually an expression or something?

return x is an expression with type ! (never type), and evaluating it results in a transfer of control.

There're only 4 kinds of statements in the language. Item(things that can be located in the root scope), let statement, macro invocation, and the expression statement. Really, the Rust is an expression oriented language.

https://doc.rust-lang.org/reference/statements.html

6 Likes

To clarify what @Hyeonu said. Since statement macros are just abbreviations for (a sequence of) other statements and items inside of functions/blocks aren't “really” part of the function/block except for visibility considerations: The rule to keep in mind is that in Rust every statement is an expression except for let statements.

5 Likes

I used to think the same, and if I look at my early Rust code it does have explicit returns in all functions/methods.

I think where I started to change my mind was when someone said something along the line of "all scopes in Rust return a value" (lazy terminology..). I.e:

{
  // .. stuff ..

  // always a return value here, but could be an implicit ()
}

From that perspective not explicitly typing out return started to make sense to me. At the same time I realized that I could rephrase this:

let mut map = HashMap::new();
// A bunch of map.insert()'s

Foo {
  inner_map: map
}

.. as this:

Foo {
  inner_map: {
    let mut map = HashMap::new();
    // A bunch of map.insert()'s
    map
  }
}
4 Likes

It's also common in Rust to have short closures sprinkled about, ala

for x in foo.map(|x| x + 2) { /* ... */ }

And as others have said, once you're using it everywhere else, it's weirder to not use it in a normal function. In languages where the function return statement is optional but the language is not so expression oriented, I would agree that the opposite applies.

Or to rephrase, exceptional syntax stands out. And in Rust, an unneeded return is exceptional; in other languages, an implicit return is exceptional.

12 Likes

If you have to grep your code for return statements, you're doing something wrong.

4 Likes

I was already aware that Rust is an expression oriented language. I just wasn't aware of the precise semantics of the return C; construct.
Specifically interesting to me is that what return is used for is not the value it yields as an expression (if those 2 things were the same, return would only ever return ! from a fn, effectively making it useless). Logical in hindsight, but a subtle point nonetheless.

By the way, these semantics can be really useful. You can use return in e.g. a match expression and type checking is still happy.

pub enum Number {
    One,
    Two,
    Three,
    NotANumber,
}
use Number::*;
pub fn to_u8_squared(x: Number) -> Option<i32> {
    let n = match x {
        One => 1,
        Two => 2,
        Three => 3,
        NotANumber => return None, // <- expression of type !, coerces to i32
    };
    Some(n * n)
}

In this context, it’s also useful to further talk about return values of blocks. A block with a final expression (i.e. without a semicolon) always has the value of that final expression as the value of the whole block. But blocks without such a final expression also have a return value: Usually it’s (), but a sometimes overlooked fact is that when the end of the block is known to be unreachable, then the block’s (return) type becomes !. And vice-versa, the presence of a value of type ! at statement/expression in a block means that everything afterwards is unreachable. Long story short, this is why the example above also works if if’s written

pub fn to_u8_squared(x: Number) -> Option<i32> {
    let n = match x {
        One => 1,
        Two => 2,
        Three => 3,
        NotANumber =>  {
            return None; // <- statement containing value of type !,
            // ^^ thus everything after this statement is unreachable
        } // <- the never-terminating block is an expression of type !, coerces to i32
    };
    Some(n * n)
}

The body of a function behaves the same way any other block does. If it doesn’t terminate, you don’t need a return value.

pub fn foo() -> i32 {
    panic!();
    // this place is unreachable
} // thus the function body has type `!` which can be coerced to the return type `i32`. 

This is also the reason why using “explicit” return works:

pub fn foo() -> i32 {
    return 42;
    // this place is unreachable
} // thus the function body has type `!` which can be coerced to the return type `i32`. 
8 Likes

And if you don't have that, then you'll need general-purpose dummy values for when you intend to return references to objects you couldn't initialize properly.

1 Like

In general I would agree, but it can be helpful to check all the early exit points from a function by searching for returns. Of course, in these cases you have to write return anyway, so it's not an issue.

2 Likes

One additional caveat is that if you have to search for early returns, your function is almost certainly too big and doing too many things.

1 Like

I guess, it depends. If you work on a complex parser, your match statement will grow quite large and you can't just split that one up. You cannot generally make that argument for every large function. Sometimes, the opposite is the case, too. Especially beginners who have just learned about code separation and re-use tend to overdo it on the code splitting part, making it actually more difficult to read the code, because you have to jump between a ton of functions to figure out how the one function at the top works.

Anyway, project organization is not the topic, so let's move back to the main discussion.


At the beginning, I was still writing a lot of returns out of habit, but now I've mostly switched away from it, whenever possible. It just saves some typing without affecting readability. No return means simple control flow. Once you get comfortable with the concept of "almost everything is an expression", you won't miss return. That said, there are some situations where return is mandatory, so it's not like it could be removed entirely.

At the end of the day, this is a fairly subjective topic. It's kinda like British vs. American English when writing localisation/localization. It doesn't matter which one you pick, as long as you stick to one and don't mix them.

1 Like

In many cases definitely yes, although in many cases you're also not the one who wrote the code :slight_smile:

2 Likes