Why is this statement returning a value when according to the rust book only expressions are supposed to return a value?

I have the following snippet from the rust book.

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

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

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

According to the rust book, a statement ends with a semicolon, and they don't return any value. Only expressions return a value.

Quoting from the rust book Chapter 3.3 Statements and Expressions

Expressions do not include ending semicolons. If you add a semicolon to the end of an expression, you turn it into a statement, and it will then not return a value.

So here in their example, break counter * 2; ends with a semi colon. I don't see a compiler error. But it returns a value to result.

Just to verify, I removed the semicolon and it still works. In fact removing the semicolon makes more sense to me.

So why does adding the semicolon still return a value back to result? can someone help explain?

loop { … } is an expression. It evaluates to the value given to the break … expression. Evaluating the break … expression, even when turned into a statement like break …;, will make control-flow immediately leave the surrounding loop expression, and make that loop expression evaluate to the provided value, as explained before.

3 Likes

But doesn't loop{...}; also end with a semicolon? Wouldn't that make it a statement again?
Ok sorry. The semicolon is not that of loop but from the let statement.

By the way, the break counter * 2 expression itself never evaluates to anything anyways. In particular it doesn’t evaluate to counter * 2. It’s similar to return … expressions in that way. To the type system, it looks like the expression actually evaluates a value of the “never” type never - Rust, but that type does not have any values. It’s a neat type-system trick to allow fancy things like let x = if … { break } else { other_value() } (where other_value() is of some type Foo and you want to (conditionally) assign x (also of type Foo) to that value, or break execution from the surrounding loop).

1 Like

No, the semicolon after the loop { … } expression is part of the let statement. Or maybe “terminates the let statement”, depending on who you ask, it’s not 100% clear whether or not the semicolon should count as “part of the statement”. The syntax of a let statement (without a type annotation, and with an initializer) goes let PATTERN = EXPRESSION; ending with a semicolon. A full description of the syntax of let and let-else can be found here in the reference: Statements - The Rust Reference

2 Likes

Okay I had tried to remove the break and just kept counter * 2, hoping it would return the evaluated answer back to result and break the loop. But that assumption was wrong, and it instead it gave a compilation error. Saying that it found an integer but expected ()

let result = loop {
        counter += 1;
        if counter == 10 { counter * 2}

I will have to go through the "never" type. Haven't really reached that section yet. So bottom line is that the break can act like a return statement, in certain statements like let

Is it safe to say that break is an expression and not a statement?

A loop without break will never terminate. The code

loop {
        counter += 1;
        if counter == 10 { counter * 2 }
}

would mean the if counter == 10 { counter * 2 } evaluates to counter * 2 (an integer) in the true case, and nothing (of type ()) in the false case (due to a lack of else branch)`, so that’s at type-mismatch already. If you added an else branch, e.g.

loop {
        counter += 1;
        if counter == 10 { counter * 2 } else { 42 }
}

then the whole if counter == 10 { counter * 2 } else { 42 } expression evaluates to an integer,

and it’s the final expression of the

{
        counter += 1;
        if counter == 10 { counter * 2 } else { 42 }
}

block which thus also evaluates to that integer. But loop expects a block of type (), which is type error yet again.

In Rust, almost everything is an expression. Really, the only kind of statement that isn’t just an expression plus the (sometimes optional) semicolon is a let statement.

So, yes, it’s safe to say that break (or break …) is an expression; and every expression can become a statement (by adding the semicolon) so break; (or break …;) would be a statement.

1 Like

Thanks, this explanation does help ease out my gripe about the semicolon defining whether something returns a value or not.

As a good demonstration that break is an expression, something like

let x: String = break;

works. It also demonstrates the “magical” property of the never type to convert implicitly to any other type, which also powers todo!(), useful for writing code that isn’t quite done yet, but you already want to check whether or not it compiles.

fn demonstration1() {
    let x: String = todo!();
}

fn demonstration2() {
    let x: String = return;
}

fn demonstration3() {
    loop {
        let x: String = break;
    }
}

fn demonstration4() {
    loop {
        let x: [i32; 4] = [1, break, 2, 3];
    }
}

fn demonstration5() {
    let x: [i32; 4] = [1, todo!(), 2, 3];
}
3 Likes

These set of snippets help understand. Thanks for sharing.

1 Like

That's not accurate. break breaks from the loop. It has nothing to do with the let; the let is a red herring. If you have a loop that contains a break, then the value of the loop is the expression specified after the break.

2 Likes

Right--you can have expressions that return a value, even if that return value is not assigned to anything.

For example:

fn main() {
    let mut counter = 0;

    loop {
        counter += 1;

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

    let result = counter * 2;
    println!("The result is {result}");
}

Here, all I've done is remove the let statement from the output of the loop, and instead assigned to result in a separate statement. The loop/break still creates an expression that returns a value, technically; but nothing is done with that value, and it is lost.

(I don't know what the compiler actually does with this--if it realizes the value is meaningless and thus optimizes out the creation of the return value--but conceptually that is what happens)

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.