Why is more clear syntax deprecated?

Is there any actual reason why

return return_value;

is deprecated in favour of

return_value

?
In my opinion, the former is more explicit and clear.

In my opinion, the latter is the clearer option of the two. :woman_shrugging:

1 Like

Explicit returns are not deprecated. It's just that Rust is an expression language, and you don't have to write out returns at the end of a function, because function bodies are block-like and as such, they automatically yield the last value.

2 Likes

It is also idiomatic Rust to do things such as

let foo = if something {
  alternative_a
} else {
  alternative_b
};

let bar = match other_thing {
  Some(_) => {
    let bunny = quz();
    bunny + 4
  }
  None => {
    let rabbit = fft(PI);
    rabbit / 7
  }
};

I.e. the pattern of evaluating a block as an expression generalizes to more than just functions. (The more you do it, the more natural it will become.)

4 Likes

I wondered about this for quite some time as well. Then I had a, perhaps obvious, realization:

A block in Rust is a list of expressions. Like this:

    {exp1; exp2; exp3; exp4; ... expN;}

where the exp's are various assignments, method calls, etc. That calculate a final result.

Looking at that we see that the last semi-colon is redundant. It serves no purpose. So we remove it like so:

    {exp1; exp2; exp3; exp4; ... expN}

Now, we would like such a block to be an expression. It should produce a result that can be assigned to something or used somehow. For example like so:

    let result = {exp1; exp2; exp3; exp4; ... expN};

It's natural that result takes on the last value produced by that list of expressions. In this case expN.

And there we have the Rust syntax for the use of semi-colons. It all follows quite naturally from the list of expressions idea.

Of course if we inadvertently put a semi-colon on the end of the list we have:

    let result = {exp1; exp2; exp3; exp4; ... expN;};

With the "list of expressions" idea in mind we now see that what we have done is to add an empty expression to the end of the list. The thing after the last semi colon. That empty expression produces an empty result (), as it should, and so the final result of our block is ().

We still have return to force an early return with a return value if we want.

7 Likes

It's not really deprecated. There's just an opinionated clippy lint about it.

In Rust (almost) everything is an expression, so it tries to favor expression-oriented style.

6 Likes

As to why is a lack of return favored: code is easier to refactor / move around if it uses as few imperative control flow statements as possible, and return is the most common offender:

fn only_called_once (arg: Arg)
  -> Ret
{
    if condition(&arg) {
        then_body(arg)
    } else {
        else_body(arg)
    }
}

fn main ()
{
    let arg: Arg = …;
    let foo =
        only_called_once(arg)
    ;
    …
}

At this point, inlining the function's body inside its only call-site is easy, one just has to cut-and-paste:

- fn only_called_once (arg: Arg)
-   -> Ret
- {
-     if condition(&arg) {
-         then_body(arg)
-     } else {
-         else_body(arg)
-     }
- }
-
  fn main ()
  {
      let arg: Arg = …;
      let foo =
-         only_called_once(arg)
+         if condition(&arg) {
+             then_body(arg)
+         } else {
+             else_body(arg)
+         }
      ;
      …
  }

But if the original function had been using return statements, then one would have had to remove those in order for the body to be moveable.

Another similar refactoring situation: we still have that function, but we suddenly want to add a logging statement to express what the return value is:

  fn foo (arg: Arg)
    -> Ret
  {
-     if condition(&arg) {
+     let ret = if condition(&arg) {
          then_body(arg)
      }
          else_body(arg)
-     }
+     };
+     ::log::debug!("`foo()` returns `{:?}`", ret);
+     ret
  }

If the ternary had been using return statements, then this kind of refactoring would have become more annoying to write.

5 Likes

Minor nitpick: You can write statements in a block, not just expressions. A statement is pretty much either an expression or a let statement, ignoring macros and nested item definitions. Also certain expressions that end in } don’t need a semicolon afterwards if they are of type (). For example

{
    if true {
        foo();
    } // no semicolon here
    bar();
    baz()
}

but these are just details. I still like the intuition you’re conveying here a lot.

1 Like

Thanks.

I was just keeping it as simple as possible to make the point. Or at least simple enough that I could understand what I wrote myself :slight_smile:

Thing is, with that insight I began to like that syntax over the use of return everywhere.

1 Like

IMO having/mixing both at function end is damaging to readability. The absence or return has been in since the beginning.

Just take clippies advice and it all comes out well.

2 Likes

Many languages have implicit return of the last value, but Rust is the only one I've used that gets it right. Say, I write in some imaginary language,

fn {
   some_expression
}

and later I modify it to be

fn {
    some_expression
    some_other_expression
}

I didn't mean to change the return value, but adding a line did exactly that. (I don't know about you, but I've made that mistake more than once.) Even a strongly typed language won't catch this error if the new return value is the same type as the old.

Rust does not have this problem. The second example won't compile because of a missing semicolon.

1 Like