Variables, Types and Functions · about 40 min

Functions, Expressions and Statements

Why this matters

Functions are how you break a problem into named pieces you can understand, test and reuse one at a time. In Rust they also reveal a key idea of the language: almost everything is an expression that produces a value, including blocks and (in the next module) if and match. Once you see that, a famous beginner puzzle disappears: why adding a semicolon to the last line of a function can turn working code into a type error.

Defining and calling functions

fn greet(name: &str, times: u32) {
    println!("Hello, {name}! (x{times})");
}

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    greet("Ada", 2);
    let sum = add(2, 3);
    println!("{sum}");
    println!("{}", add(sum, 10));
}
Hello, Ada! (x2)
5
15

&str is the type of a string literal like "Ada"; module 6 explains it fully.

Statements and expressions

Rust distinguishes two kinds of code:

fn main() {
    let a = 5;          // statement (let)
    a + 1;              // statement: computes 6 and discards it (compiler warns)
    let b = a + 1;      // the expression a + 1 inside a let statement
    println!("{b}");    // a macro call expression, made a statement by ;
}

Because let is a statement and has no value, you cannot write let x = (let y = 6); as you might x = y = 6 in C.

Blocks are expressions

A block { ... } contains zero or more statements followed by an optional final expression without a semicolon. The block's value is that final expression, or () if there is none.

fn main() {
    let x = 4;
    let y = {
        let doubled = x * 2;
        doubled + 1
    };
    let z = {
        let doubled = x * 2;
        doubled + 1;
    };
    println!("{y} {:?}", z);
}
9 ()

y is 9. z is (): its block ends in a statement, so it has no final expression. (The compiler warns about the unused value, a strong hint you did not mean it.)

Function bodies return their final expression

A function body is a block, so the function returns the value of its final expression:

fn area(width: f64, height: f64) -> f64 {
    width * height
}

No return keyword is needed, and idiomatic Rust leaves it out at the end. Now the puzzle. Add a semicolon:

fn area(width: f64, height: f64) -> f64 {
    width * height;
}

fn main() {
    println!("{}", area(2.0, 3.0));
}
error[E0308]: mismatched types
 --> main.rs:1:37
  |
1 | fn area(width: f64, height: f64) -> f64 {
  |    ----                             ^^^ expected `f64`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
2 |     width * height;
  |                   - help: remove this semicolon to return this value

With the semicolon, width * height; is a statement, the block has no final expression, so it evaluates to (), but the signature promised f64. The message says exactly that ("implicitly returns () as its body has no tail or return expression") and the help tells you to remove the semicolon. Read it once carefully and you will recognize it instantly from now on.

return for leaving early

return value; exits the function immediately. It is used for early exits, typically guarding against special cases before the main computation:

fn safe_div(a: i32, b: i32) -> i32 {
    if b == 0 {
        return 0;
    }
    a / b
}

fn main() {
    println!("{} {}", safe_div(10, 3), safe_div(1, 0));
}
3 0

(if gets its own lesson in the next module; the meaning here should be clear.) Returning 0 for division by zero is a questionable design that hides an error; module 8 shows the proper way with Option and Result.

Returning several values

Return a tuple and destructure it at the call site:

fn min_max(a: i32, b: i32, c: i32) -> (i32, i32) {
    (a.min(b).min(c), a.max(b).max(c))
}

fn main() {
    let (lo, hi) = min_max(7, -2, 5);
    println!("min {lo}, max {hi}");
}
min -2, max 7

What happens when you call a function

Each call gets its own stack frame holding its parameters and local variables. Calls nest, so frames pile up on the call stack and are removed in reverse order as functions return:

fn main()  calls  add(2, 3)

while add runs:                      after add returns:

 +--------------------+              +--------------------+
 | add:  a = 2        |  <- top      | main: sum = 5      |
 |       b = 3        |              +--------------------+
 |       (return addr)|
 +--------------------+
 | main: sum = ?      |
 +--------------------+

When main calls add(2, 3), the argument values are copied into add's parameters a and b, the CPU remembers where to come back to, and jumps to add's code. When add finishes, its result is passed back (usually in a CPU register), its frame disappears, and main continues with sum set to 5. Parameters are local to the call: changing a parameter inside a function never changes the caller's variable. (For a parameter you want to modify, write mut before its name: fn f(mut n: i32). It still only changes the local copy.)

That "copied into the parameters" is true for simple values like integers. For values that own memory, such as text read from input, passing to a function moves them, and that is the subject of module 4. The stack picture you have here is the foundation it builds on.

Common mistakes

Missing parameter types: fn add(a, b) is an error: "expected one of :, @, or |". Every parameter needs name: Type.

The trailing semicolon in a function that returns a value: E0308 with "implicitly returns ()", as shown above.

Wrong number or type of arguments:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    println!("{}", add(1));
}
error[E0061]: this function takes 2 arguments but 1 argument was supplied
 --> main.rs:6:20
  |
6 |     println!("{}", add(1));
  |                    ^^^--- argument #2 of type `i32` is missing
  |
note: function defined here
 --> main.rs:1:4
  |
1 | fn add(a: i32, b: i32) -> i32 {
  |    ^^^ ------  ------
help: provide the argument
  |
6 |     println!("{}", add(1, /* i32 */));
  |                       ~~~~~~~~~~~~~~

E0061 shows the signature it expected and suggests the missing argument.

Ignoring a return value. Calling add(1, 2); as a statement computes 3 and discards it. Rust does not warn for plain functions, but many library functions are marked so that ignoring their result does produce a warning (unused_must_use).

Recap

Going further: write fn main() -> () {} and see that it compiles: () is the return type you always had. Look up rustc --explain E0308 again; you now understand all of its examples.

Exercises

Exercise 1 · write code

Your first function

Write a function square that takes an i64 and returns its square as an i64. Keep the main given in the starter; it prints:

9
144

Hidden tests also call square directly with other values, including negative ones.

File: main.rs

Starting point

// write fn square here

fn main() {
    println!("{}", square(3));
    println!("{}", square(-12));
}

Example

expected output

9
144
Hint 1

The signature is fn square(x: i64) -> i64.

Hint 2

The body can be the single expression x * x, with no semicolon.

Reference solution
fn square(x: i64) -> i64 {
    x * x
}

fn main() {
    println!("{}", square(3));
    println!("{}", square(-12));
}

Exercise 2 · write code

The semicolon that changes the type

This program fails with error E0308. Read the message (especially its help: line), fix average, and make the program print:

2.5

File: main.rs

Starting point

fn average(a: f64, b: f64) -> f64 {
    (a + b) / 2.0;
}

fn main() {
    println!("{}", average(2.0, 3.0));
}

Example

expected output

2.5
Hint 1

With a semicolon, the last line is a statement and the block's value is ().

Hint 2

Remove the semicolon after (a + b) / 2.0.

Reference solution
fn average(a: f64, b: f64) -> f64 {
    (a + b) / 2.0
}

fn main() {
    println!("{}", average(2.0, 3.0));
}

Exercise 3 · write code

Returning two values

Write fn divmod(a: u32, b: u32) -> (u32, u32) returning the quotient and remainder of a / b. In main, destructure the result of divmod(17, 5) and print:

17 = 5 * 3 + 2

Hidden tests check divmod with other inputs.

File: main.rs

Starting point

fn main() {
}

Example

expected output

17 = 5 * 3 + 2
Hint 1

Return a tuple: the last expression of the body can be (a / b, a % b).

Hint 2

let (q, r) = divmod(17, 5); unpacks it.

Reference solution
fn divmod(a: u32, b: u32) -> (u32, u32) {
    (a / b, a % b)
}

fn main() {
    let (q, r) = divmod(17, 5);
    println!("17 = 5 * {q} + {r}");
}

Exercise 4 · quiz

What is the value of the block?

What does this print?

fn main() {
    let x = 4;
    let y = {
        let x = x * 2;
        x + 1
    };
    println!("{x} {y}");
}
Answer
  • 4 9

A block { ... } is an expression: its value is the value of its final expression, here x + 1 where the inner x is 8. So y is 9. The inner let x shadows only inside the block; afterwards x is the outer 4 again. If the final line had been x + 1; (with a semicolon), the block's value would be () instead.

Exercise 5 · write code

Early return

Write fn percent(part: u32, whole: u32) -> u32 that returns part * 100 / whole, except that when whole is 0 it must return 0 immediately (use if whole == 0 { return 0; } as the first line). Keep the given main, which prints:

25
0
100

File: main.rs

Starting point

fn percent(part: u32, whole: u32) -> u32 {
    part * 100 / whole
}

fn main() {
    println!("{}", percent(1, 4));
    println!("{}", percent(5, 0));
    println!("{}", percent(7, 7));
}

Example

expected output

25
0
100
Hint 1

The starter panics on the second call: attempt to divide by zero.

Hint 2

return 0; leaves the function at once with the value 0.

Reference solution
fn percent(part: u32, whole: u32) -> u32 {
    if whole == 0 {
        return 0;
    }
    part * 100 / whole
}

fn main() {
    println!("{}", percent(1, 4));
    println!("{}", percent(5, 0));
    println!("{}", percent(7, 7));
}