Hello World and Program Structure

Statements and Expressions

A statement is an instruction that performs an action and does not produce a value. An expression is code that evaluates to a value. Rust is an expression-based language: most constructs, including blocks, if, match, and loops, are expressions.

fn main() {
    let x = 5;
    let y = {
        let a = 3;
        a + 1
    };
    let z = if x > 3 { "big" } else { "small" };
    println!("{x} {y} {z}");
}
5 4 big

Each let line is a statement. The right-hand sides 5, { ... }, and if ... else ... are expressions, and each one produces the value that the let stores.

Statements

Rust has four kinds of statements.

Kind Example Purpose
Let statement let a = 5; Introduces a variable binding
Item statement fn helper() {}, struct P;, use std::fmt; Declares an item: function, type, constant, import
Expression statement foo();, if x { ... } Evaluates an expression for its effect and discards the value
Macro statement println!("hi"); Invokes a macro in statement position
fn square(n: i32) -> i32 {
    n * n
}

fn main() {
    // Item statement: a function defined inside another function.
    fn double(n: i32) -> i32 {
        n * 2
    }

    // Let statements.
    let a = square(4);
    let b = double(a);

    // Expression statements.
    println!("a = {a}, b = {b}");
    if b > 20 {
        println!("b is large");
    }
}
a = 16, b = 32
b is large

An item declared inside a function is visible only inside the block that contains it.

A let Statement Is Not an Expression

let produces no value, so it cannot appear where a value is expected. This differs from C and Ruby, where int a = b = 6; is valid.

// ❌ does not compile
fn main() {
    let a = let b = 6;
}
error: expected expression, found `let` statement
 --> a3.rs:3:13
  |
3 |     let a = let b = 6;
  |             ^^^
  |
  = note: only supported directly in conditions of `if` and `while` expressions

The note refers to if let and while let, where let is part of a condition. That use is a pattern-matching condition, not a let statement.

Expressions

An expression evaluates to a value. Almost everything that is not a let statement or an item is an expression.

Expression Example Value
Literal 42, "text", true The literal itself
Variable x The value stored in x
Operator a + b, !flag, x > 1 The result of the operation
Function call square(4) The returned value
Method call v.len() The returned value
Macro call vec![1, 2], format!("{n}") The value the macro expands to
Block { let a = 1; a + 1 } The value of its last expression
if / else if c { 1 } else { 2 } The value of the branch taken
match match n { 0 => "zero", _ => "other" } The value of the arm taken
loop loop { break 5; } The value given to break
Closure |a, b| a + b A closure value
Range 0..10 A range value
Assignment x = 5 ()

Expressions nest. Every part of a larger expression is itself an expression:

fn main() {
    let total = {
        println!("computing");
        let base = 10;
        base * 2
    };
    println!("total = {total}");

    let v = vec![1, 2, 3];
    let len = v.len();
    let doubled: Vec<i32> = v.iter().map(|n| n * 2).collect();
    let closure_result = (|a: i32, b: i32| a + b)(2, 3);
    println!("{len} {doubled:?} {closure_result}");
}
computing
total = 20
3 [2, 4, 6] 5

Control Flow as Expressions

if, match, and loop produce values, so no separate ternary operator is needed.

fn main() {
    let n = 7;
    let kind = if n % 2 == 0 { "even" } else { "odd" };

    let size = match n {
        0 => "zero",
        1..=9 => "small",
        _ => "large",
    };

    let mut counter = 0;
    let first_over_ten = loop {
        counter += 3;
        if counter > 10 {
            break counter;
        }
    };

    let label = {
        let prefix = "n";
        format!("{prefix}={n}")
    };

    println!("{kind} {size} {first_over_ten} {label}");
}
odd small 12 n=7

All branches of an if or match used as a value must have the same type:

// ❌ does not compile
fn main() {
    let n = 3;
    let label = if n > 2 { "big" } else { 0 };
}
error[E0308]: `if` and `else` have incompatible types
 --> b8.rs:4:43
  |
4 |     let label = if n > 2 { "big" } else { 0 };
  |                            -----          ^ expected `&str`, found integer
  |                            |
  |                            expected because of this

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0308`.

An if without an else evaluates to () when the condition is false, so it cannot produce any other type:

// ❌ does not compile
fn main() {
    let n = 3;
    let label = if n > 2 { "big" };
}
error[E0317]: `if` may be missing an `else` clause
 --> b9.rs:4:17
  |
4 |     let label = if n > 2 { "big" };
  |                 ^^^^^^^^^^^-----^^
  |                 |          |
  |                 |          found here
  |                 expected `&str`, found `()`
  |
  = note: `if` expressions without `else` evaluate to `()`
  = help: consider adding an `else` block that evaluates to the expected type

The Unit Value

An expression that has nothing meaningful to produce evaluates to (), the unit value. It is the only value of the unit type, which is also written ().

  • A function without a return type returns ().
  • println! returns ().
  • An if without else evaluates to ().
  • An assignment evaluates to ().
fn main() {
    let a = 1;
    let b = 2;
    let unit = ();
    let no_value = if a > b { println!("a is larger") };
    println!("{unit:?} {no_value:?}");
}
() ()

Assignment Is an Expression of Type ()

In C, a = b = 3 assigns 3 to both variables because the assignment expression returns the assigned value. In Rust an assignment evaluates to (), so the same code does not work.

fn main() {
    let mut x = 1;
    println!("x = {x}");
    let result = x = 5;
    println!("result = {result:?}, x = {x}");
    let compound = x += 1;
    println!("compound = {compound:?}, x = {x}");
}
x = 1
result = (), x = 5
compound = (), x = 6
// ❌ does not compile
fn main() {
    let mut a = 1;
    let mut b = 2;
    a = b = 3;
}
error[E0308]: mismatched types
 --> a5.rs:5:9
  |
3 |     let mut a = 1;
  |                 - expected due to this value
4 |     let mut b = 2;
5 |     a = b = 3;
  |         ^^^^^ expected integer, found `()`

Blocks and the Tail Expression

A block { ... } is an expression. It contains zero or more statements, optionally followed by one expression with no semicolon. That last expression is the tail expression, and its value becomes the value of the block. A block with no tail expression evaluates to ().

{
    let a = 3;       <- statement
    let b = a * 2;   <- statement
    a + b            <- tail expression: no semicolon, value of the block
}

A function body is a block, so the tail expression is the function's return value:

fn add_one(n: i32) -> i32 {
    n + 1
}

fn main() {
    println!("{}", add_one(1));
}
2

return is needed only for returning early. Adding a semicolon to the tail expression turns it into a statement. The block then has no tail expression and evaluates to ():

// ❌ does not compile
fn add_one(n: i32) -> i32 {
    n + 1;
}

fn main() {
    println!("{}", add_one(1));
}
error[E0308]: mismatched types
 --> b6.rs:2:23
  |
2 | fn add_one(n: i32) -> i32 {
  |    -------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
3 |     n + 1;
  |          - help: remove this semicolon to return this value

Expression Statements

Placing a semicolon after an expression makes it an expression statement. The expression is evaluated and its value is discarded.

fn main() {
    let x = 3;
    if x > 1 { 10 } else { 20 };
    1 + 2;
    x;
    "text";
    println!("{x}");
}
warning: unused arithmetic operation that must be used
 --> a7.rs:4:5
  |
4 |     1 + 2;
  |     ^^^^^ the arithmetic operation produces a value
  |
  = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
  |
4 |     let _ = 1 + 2;
  |     +++++++

warning: path statement with no effect
 --> a7.rs:5:5
  |
5 |     x;
  |     ^^
  |
  = note: `#[warn(path_statements)]` (part of `#[warn(unused)]`) on by default

warning: 2 warnings emitted

3

The code compiles, but the compiler warns about expression statements that compute a value and do nothing with it.

Block-Like Expressions as Statements

if, match, loop, while, for, and plain blocks can be statements without a trailing semicolon. In that position they must evaluate to ():

// ❌ does not compile
fn main() {
    let x = 3;
    if x > 1 { 10 } else { 20 }
    println!("{x}");
}
error[E0308]: mismatched types
 --> a6.rs:4:16
  |
4 |     if x > 1 { 10 } else { 20 }
  |     -----------^^--------------
  |     |          |
  |     |          expected `()`, found integer
  |     expected this to be `()`

error[E0308]: mismatched types
 --> a6.rs:4:28
  |
4 |     if x > 1 { 10 } else { 20 }
  |     -----------------------^^--
  |     |                      |
  |     |                      expected `()`, found integer
  |     expected this to be `()`

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0308`.

Adding a semicolon after the closing brace, as in the previous example, discards the value and fixes the error.

Diverging Expressions

Some expressions never produce a value because they transfer control elsewhere: return, break, continue, panic!, and calls to functions that never return. Their type is !, the never type, which can be used in place of any other type. This lets one branch of an if or match leave the surrounding code while the other produces a value.

fn describe(n: i32) -> &'static str {
    let text = if n > 0 {
        "positive"
    } else {
        return "not positive";
    };
    text
}

fn main() {
    let values = [3, -1, 4];
    for value in values {
        let doubled: i32 = if value > 0 { value * 2 } else { continue };
        println!("{value} -> {doubled}");
    }
    println!("{}", describe(5));
    println!("{}", describe(-5));
}
3 -> 6
4 -> 8
positive
not positive

In describe, the else branch returns from the function, so only the "positive" branch produces the value of text. In the loop, continue skips the rest of the iteration for negative numbers.

Place Expressions and Value Expressions

Rust distinguishes two categories of expression:

Category Meaning Examples
Place expression Refers to a memory location. It can appear on the left side of an assignment x, numbers[0], point.x, *reference
Value expression Produces a value and refers to no location 5, a + b, square(4)

Only a place expression can be assigned to:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let mut numbers = [1, 2, 3];
    let mut point = Point { x: 0, y: 0 };
    let mut value = 10;
    let reference = &mut value;

    numbers[0] = 100; // index expression is a place
    point.x = 5; // field access is a place
    *reference = 20; // dereference is a place
    point.y = point.x + 1; // right side is a value

    println!("{numbers:?} ({}, {}) {}", point.x, point.y, value);
}
[100, 2, 3] (5, 6) 20

Assigning to a value expression is an error:

// ❌ does not compile
fn main() {
    let x = 1;
    5 = x;
}
error[E0070]: invalid left-hand side of assignment
 --> c1.rs:4:7
  |
4 |     5 = x;
  |     - ^
  |     |
  |     cannot assign to this expression