Welcome to the fourth lesson in our journey through Rust. In our previous session, we demystified Rust's ahead-of-time (AOT) compilation model by manually compiling and running a "Hello, World!" program. You saw that the line println!("Hello, world!"); ended with a semicolon, which we noted marked the end of an expression.
Today, we'll delve into that observation and explore one of the most fundamental and powerful concepts in Rust: the distinction between statements and expressions. Understanding this is not just a matter of syntax; it's key to grasping Rust's design philosophy and writing idiomatic code. Since you're familiar with Swift, this concept of an "expression-oriented" language might have some familiar echoes, particularly regarding implicit returns.
1. The Core Distinction: Actions vs. Values
At its heart, the difference is simple:
- Statements are instructions that perform some action but do not return a value.
- Expressions are instructions that evaluate to a resultant value.
Let's begin by reading the official definitions and seeing a classic example from "The Rust Programming Language" book.
Functions - The Rust Programming Language
This section from the official Rust book provides the canonical definitions for statements and expressions, along with clear, illustrative code examples.
Please read the section from the beginning of "Statements and Expressions". Pay close attention to: The formal definitions of statements and expressions. The example let y = 6; as a statement. The explanation for why you cannot assign a let statement to another variable (let x = (let y = 6);). This is a key difference from languages like Python or C, where assignments can be chained. The list of things that are expressions: math operations, function calls, macro calls, and importantly, code blocks created with {}.
As the text highlights, creating a variable with let is a statement. It performs an action—binding a value to a name—but the statement itself doesn't evaluate to a value. This is why let x = (let y = 6); fails. There is no value produced by let y = 6 for x to bind to.
In contrast, 5 + 6 is an expression. It doesn't do anything on its own, but it evaluates to the value 11. This value can then be used, for example, as part of a statement: let result = 5 + 6;. Here, 5 + 6 is the expression, and the entire line let result = ...; is the statement.
For a quick visual and auditory reinforcement of these core ideas, let's watch a short video.
13: Statements vs expressions in Rust
This video from the channel Rustfully provides a concise summary of the topic.
Watch from the beginning. The presenter first gives the verbal definitions and then provides examples of statements, such as let bindings and println!, contrasting Rust's behavior with other languages where assignment is an expression.
2. The Power of the Semicolon
Now for the crucial detail that ties this all together: the semicolon. In many languages, a semicolon is simply a required line terminator. In Rust, its role is more nuanced and powerful: a semicolon turns an expression into a statement.
When an expression is followed by a semicolon, you are telling the compiler, "Evaluate this expression, but throw away the resulting value." The entire line now becomes a statement, performing an action but producing no value.
This has profound implications, especially when combined with another feature you saw mentioned in the book: code blocks ({...}) are expressions. A code block evaluates to the value of the final expression within it.
Let's see a practical demonstration of how a single semicolon can change the meaning of your code.
Rust Tutorial #8 - Functions, Expressions & Statements
This video from Tech With Tim gives an excellent demonstration of code blocks as expressions and the critical role of the semicolon.
Watch the segment from here. Pay close attention to two moments: The setup where a block { let x = 3; x + 1 } is assigned to a variable. Notice there is no semicolon after x + 1. The moment around this timestamp where he adds a semicolon to x + 1; and the code no longer compiles. This is the key takeaway.
The error message in the video might be a bit cryptic right now, but it's pointing to a fundamental truth. When the semicolon is added, the block { let x = 3; x + 1; } no longer evaluates to 4. It now evaluates to... nothing.
3. The "Nothing" Value: Unit Type ()
So what does a statement, or a block ending with a semicolon, evaluate to? In Rust, this "nothing" has a name and a type: the unit type, written as (). It's an empty tuple that represents the absence of a meaningful value.
This concept is essential. When the compiler expected an integer from the code block in the video but got () instead, it reported a type mismatch error. Let's see this explained in the official book.
Functions - The Rust Programming Language
This part of the book explicitly discusses the effect of adding a semicolon to an expression, particularly in the context of function returns.
Read the two paragraphs starting from this point. The example shows a function that is supposed to return an i32. When a semicolon is added to x + 1;, the function implicitly returns () instead, causing the mismatched types error.
This "expression-oriented" design allows for very concise and clear code. For example, if-else constructs are also expressions in Rust. This allows you to write code like this, which should feel familiar from Swift:
let condition = true;
let number = if condition { 5 } else { 6 }; // `number` will be 5
Here, the entire if-else block is an expression that evaluates to a value, which is then bound to the number variable. This is often cleaner than declaring a mutable variable and assigning to it inside separate if and else blocks.
If you leave out the else, and the if condition is false, what does the expression evaluate to? You guessed it: (). Therefore, for an if without an else to be valid in this context, the if block must also evaluate to () (e.g., by ending with a semicolon).
Conclusion
In this lesson, we've unpacked a core design principle of Rust that sets it apart from many other languages. Mastering this concept is a significant step toward thinking like a Rust programmer.
Here are the main takeaways:
- Statements perform an action and do not return a value.
letbindings are a prime example. - Expressions evaluate to a resulting value. Math operations, function calls, and code blocks are all expressions.
- The semicolon is the key differentiator: it transforms an expression into a statement by discarding its value.
- Statements and expressions that produce no value evaluate to the unit type
(). - This "expression-oriented" nature allows constructs like
if-elseand code blocks to be used in powerful ways to return values directly.
In our next lesson, we will continue to build on this foundation. We will look more closely at the let statement, exploring Rust's "immutability by default" philosophy and how to declare both immutable and mutable variables.