Chapter 4

Functions

👋 Anyone can read and edit this exercise. Sign up to save your progress.

The elephant in the room is that you've been writing a function since chapter one... 😬👉👈

Every fn main() { ... } is one. And every println!(...) is a call, though println! is a macro, which is why it has the !. So this chapter isn't really introducing functions. It's introducing the parts of fn we've quietly skipped: parameters, return types, the body, and a few small Rust-specific quirks.

Anatomy

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

Reading left to right: fn says "this is a function", add is its name, the parentheses list the parameters with their types, and -> i32 declares the return type. Every parameter type and the return type are spelled out explicitly. Rust never guesses these for you.

To call a function, write its name with the arguments in parentheses:

let sum = add(2, 3);   // sum: i32 = 5

Expressions, not statements

The body of a function is a block, and a block is one or more statements followed by an optional final expression. The final expression (if there is one, and it has no trailing semicolon) becomes the value of the block, which becomes the return value of the function.

fn double(n: i32) -> i32 {
    n * 2          // no semicolon: this is the return value
}

You can also use an explicit return, which is occasionally useful for early exits, but the no-semicolon form is more idiomatic for the final value:

fn double(n: i32) -> i32 {
    return n * 2;  // works too, but unusual at the end
}

That semicolon thing trips up newcomers. The rule is short: a semicolon turns an expression into a statement (which has no value). Forgetting one at the end of the function is the correct thing to do when you want the value to be returned. Adding one accidentally turns the body into "do this, then return ()" and the compiler will complain that the types don't match. The first exercise lets you feel that error first-hand.

The three exercises after that each pull at one more thread: returning nothing, returning a value built recursively, and modifying a parameter inside the body.

A few good habits

A stray semicolon

This function takes an i32, declares an i32 return type, multiplies by two. Yet, the compiler refuses to compile.

Run the tests, read the error, and fix it.

The lesson hiding behind that error is the difference between an expression (which has a value) and a statement (which doesn't). One character decides which a line is, and that character decides what your function returns.

Exercise 1 of 4
Open in Web Editor

Results

    Compiler / runtime output
    
                

    Countdown

    Time to write a function from scratch. countdown(n) should print the numbers from n down to 1, each on its own line, and then print "Liftoff!". If n == 0, it should print only "Liftoff!".

    countdown(3)
    3
    2
    1
    Liftoff!
    

    This exercise pulls in two new ideas.

    Returning nothing

    Not every function hands back a value. countdown exists for its side effect (printing), so there's nothing meaningful to return. In Rust, that means the return type is (), the unit type, an empty tuple. You don't have to write it. A function with no -> Type is implicitly -> ():

    fn greet() {
        println!("hi");
    }
    // equivalent to:
    fn greet() -> () {
        println!("hi");
    }
    

    fn main() is exactly this: a function that returns (). You've been writing one since chapter one.

    Recursion

    A function in Rust can call itself. That's recursion, and it's the technique you'll use here instead of a loop.

    A recursive function has two parts:

    For countdown, the base case is n == 0 (just print "Liftoff!"). The recursive case prints n and then calls countdown with n - 1. Each call gets its own fresh parameters, stacked on top of the previous one, until the base case unwinds the whole stack.

    Useful from the standard library

    • println! prints a line. You've already used it in main. It's a macro (note the !), but you call it just like a function.
    Exercise 2 of 4
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  
      Stuck? Show a hint No spoilers, just a nudge
      1. The body fits in one if/else. One branch is the base case, the other is the recursive case.
      2. The base case is n == 0 and just prints "Liftoff!". The recursive case prints n and calls countdown(n - 1).
      3. fn countdown(n: u32) {
            if n == 0 {
                println!("Liftoff!");
            } else {
                println!("{n}");
                countdown(n - 1);
            }
        }
        

      Sum to N

      Now do recursion the other way around. Instead of printing as you go, return a value built up from smaller answers.

      Write sum_to(n) so it returns 1 + 2 + ... + n, with sum_to(0) == 0.

      The base case and recursive case look like this:

      sum_to(0) = 0                    // base case
      sum_to(n) = n + sum_to(n - 1)    // for n > 0
      

      This is the lightbulb moment for recursion: a function's answer can be defined in terms of its own answer to a smaller version of the same problem. Once the base case is reached, every pending call finishes its addition and the final total bubbles back up.

      Exercise 3 of 4
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    
        Stuck? Show a hint No spoilers, just a nudge
        1. Same recursion pattern as countdown: an if with a base case (n == 0) and a recursive case that calls sum_to(n - 1).
        2. The recursive case returns n + sum_to(n - 1). No mut, no let, no return.
        3. fn sum_to(n: u32) -> u32 {
              if n == 0 { 0 } else { n + sum_to(n - 1) }
          }
          

        Cap at a maximum

        Write cap_at(value, max) so that it returns value if it's at or below max, and max otherwise. Both arguments are i32. The logic is one if away.

        Write the function the most natural way you can think of. The most natural way doesn't compile. Read the error carefully before changing anything; it tells you exactly what's wrong, and the fix is one keyword in one place.

        Exercise 4 of 4
        Open in Web Editor

        Results

          Compiler / runtime output
          
                      
          Stuck? Show a hint No spoilers, just a nudge
          1. The compiler complains about assigning to value. Function parameters are immutable bindings by default, just like let.
          2. Add mut to the parameter binding (not the type): fn cap_at(mut value: i32, max: i32) -> i32.
          3. fn cap_at(mut value: i32, max: i32) -> i32 {
                if value > max {
                    value = max;
                }
                value
            }
            

          Wrapping up functions

          Four small exercises, four new ideas:

          The thread running through all of this: the body of a function is a block whose final expression (no trailing semicolon) is the return value. Everything else in the chapter is a consequence of that one rule.

          Next chapter 5Exercise Break: Word Count