9

Excerpt from Chapter 3.5 of the Rust Book:

we use the break keyword with the value counter * 2. After the loop, we use a semicolon to end the statement that assigns the value to result.

Plus the code snippet:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {}", result);
}

I understand how this works and why the result is 20, but I noticed that if I remove the semicolon on the line that contains the break keyword, the program is equivalent.

Why is the semicolon optional in this case?

pretzelhammer
  • 13,874
  • 15
  • 47
  • 98
Paul Razvan Berg
  • 16,949
  • 9
  • 76
  • 114
  • https://lucumr.pocoo.org/2012/10/18/such-a-little-thing/ – coredump Nov 26 '20 at 15:06
  • Probably because `break counter * 2` is the final expression in `if`'s block. This may help: [Are semicolons optional in rust?](https://stackoverflow.com/questions/26665471/are-semicolons-optional-in-rust). I am not sure how exactly `break` behaves but this is generally what happens with other expressions and it seems to be the same case. – Mihir Luthra Nov 26 '20 at 15:18
  • I think, `break` expression has never type (https://github.com/rust-lang/rfcs/blob/master/text/1216-bang-type.md). So never type coerces to any other type, and when `break` is the last expression of if block, it coerces to the `()` which is the type of independent expressions. – Angelicos Phosphoros Nov 27 '20 at 14:35

2 Answers2

8

A shorter example:

let _: i32 = loop {
    if true {
        break 3; // ()
    }
};

That is just another example where the semi-colon does not interfere with the intended outcome. For one, the insertion of a semi-colon introduces an expression statement, which evaluates to the unit type (). As the loops and if expressions continue to admit a code block evaluating to the same type (), then all types are in conformance.

let _: i32 = loop {
    if true {
        break 3 // ! coerced to ()
    }
};

If the semi-colon is taken away, the break is evaluated to the never type !, which coerces to any other type. This means that it will fulfill any type expected by the outer scope. In this case, break 3 alone also becomes () to fulfill the if expression. So all is well all the same, so long as you don't try to append any other statement before the end of the if block.

Both break and return evaluate to !, as their side effects imply that the program will not go through with the natural workflow.

See also:

E_net4
  • 27,810
  • 13
  • 101
  • 139
  • Sorry, in `this means that it will fulfill any type expected by the outer scope`, does the "outer scope" refer to the inside of the `loop` (so you're saying the `break 3` fulfills the expected type from the empty `else{}`, which is `()`, because `!` can coerce to `()`)? Or does it refer to the scope in which the variable `_` was created (meaning that the `break 3` fulfills `i32` and is somehow converted to `3i32` when being assigned to `_`)? – Telescope Feb 17 '23 at 15:38
  • @Telescope answer updated. The outer scope here is the block in the `if` expression, so `()` is assumed. – E_net4 Feb 17 '23 at 15:48
5

The Rust Language Reference on Expression Statements:

An expression statement is one that evaluates an expression and ignores its result. As a rule, an expression statement's purpose is to trigger the effects of evaluating its expression.

An expression that consists of only a block expression or control flow expression, if used in a context where a statement is permitted, can omit the trailing semicolon.

I imagine this is purely for aesthetics and ergonomics, since almost everything is an expression in Rust. If the trailing semicolon was mandatory after all expressions then we'd have to terminate if-else blocks (which are also expressions) with semicolons which looks terrible:

if {
    // do something
} else {
    // do something else
}; // <- gross

Similarly, we can omit the trailing semicolon on all control flow expressions because they produce control flow, so the typical function of the semicolon, which is to discard the expression's result and evaluate to () instead, becomes irrelevant.

fn five() -> i32 {
    return 5 // to semicolon or not to semicolon? it doesn't matter
}

In the above example it doesn't make a difference if we terminate return 5 with a semicolon or not since nothing can "capture" the result of that expression anyway since it produces control flow. Same would be true for other control flow expressions like break and continue.

pretzelhammer
  • 13,874
  • 15
  • 47
  • 98
  • 1
    Nice explanation. Let me get this right though - if we were to remove "return" from "return 5", the semicolon would become mandatory, right? The program wouldn't compile otherwise. – Paul Razvan Berg Nov 26 '20 at 17:58
  • 3
    @PaulRazvanBerg opposite, actually. Without keyword `return`, 5 would be returned implicitly, and a semicolon would suppress that return, returning `()` instead, which would be incompatible with the function signature. – Ivan C Nov 26 '20 at 18:03
  • 1
    Damn it, that's what I meant. The semicolon would have to be removed. Thanks for the correction @IvanC. – Paul Razvan Berg Nov 26 '20 at 18:05
  • Wait, so for control-flow expressions, adding a semicolon is equivalent to not adding a semicolon? In the above answer, though, `break 5` and `break 5;` seem to have different underlying behaviors (the first becomes a `!` which coerces to `i32`, and the second becomes `()` but the loop return `i32` anyways)? – Telescope Feb 17 '23 at 01:48