Chapter 5

Functions

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

You've been inside a function since the first line you wrote. fn main() is one, and every println!(...) is a call (the ! marks it as a macro). Functions are already familiar, so the focus is on Rust's explicit parameter and return types, blocks as expressions, and the trailing-semicolon rule that decides what gets returned.

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: zero or more statements followed by an optional final expression. When that final expression has no trailing semicolon, its value becomes the value of the block. For a function body, that block value is also the function's return value.

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. In the first exercise, you'll make that error disappear by changing a single character.

A few good habits

A stray semicolon

This function takes an i32, promises to return an i32, and multiplies the input by two. Still, the compiler refuses to compile it.

Run the tests and read the error before you change anything.

The error points to the difference between an expression, which has a value, and a statement, which doesn't. One character decides which one the final line is, and therefore what the function returns.

Exercise 1 of 3
Open in Web Editor

Results

    Compiler / runtime output
    
                
    Reveal the full solution Spoiler: the complete answer
    /// Doubles `n`.
    ///
    /// The exercise version is missing its return value: `n * 2;` with a
    /// trailing semicolon is a statement, so the function returns `()`
    /// instead of `i32`. Dropping the semicolon makes `n * 2` the final
    /// expression, which is what gets returned.
    fn double(n: i32) -> i32 {
        n * 2
    }
    
    #[test]
    fn test_double() {
        assert_eq!(double(0), 0);
        assert_eq!(double(3), 6);
        assert_eq!(double(-7), -14);
    }
    

    Sum to N

    This exercise is about recursion: a function that calls itself. Each call returns a value built up from the answer to a smaller version of the same problem.

    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
    

    That's the whole idea of recursion: a function's answer is 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 2 of 3
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  
      Stuck? Show a hint No spoilers, just a nudge
      1. The body is 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.
      Reveal the full solution Spoiler: the complete answer
      /// Returns the sum `1 + 2 + ... + n`, computed recursively.
      /// By convention, `sum_to(0)` is `0`.
      ///
      /// Each call returns its number plus the sum of everything below it;
      /// `n == 0` is the base case that returns `0` and stops the recursion.
      fn sum_to(n: u32) -> u32 {
          if n == 0 { 0 } else { n + sum_to(n - 1) }
      }
      
      #[test]
      fn test_sum_to() {
          assert_eq!(sum_to(0), 0);
          assert_eq!(sum_to(1), 1);
          assert_eq!(sum_to(3), 6); // 1 + 2 + 3
          assert_eq!(sum_to(10), 55);
          assert_eq!(sum_to(100), 5_050);
      }
      

      Cap at a maximum

      Write cap_at(value, max) so 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. Your first version may not compile, and that failure is part of the exercise. Read the error before changing anything because it tells you why the assignment is rejected.

      Once it compiles, look at the second test. The caller's variable is untouched even though the function reassigned its parameter. That's because i32 is Copy, so the function received its own copy to mutate. You saw the other half with moves: a non-Copy type such as String is moved in instead of copied. Borrowing lets a function use a value without taking ownership.

      Exercise 3 of 3
      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.
        Reveal the full solution Spoiler: the complete answer
        /// Returns `value` if it is at or below `max`, otherwise `max`.
        ///
        /// The catch is the keyword `mut`: to reassign the parameter inside the
        /// function it has to be declared `mut value`. Because `i32` is `Copy`,
        /// the function mutates its own copy, so the caller's variable is left
        /// untouched (see `caller_value_is_unchanged`).
        fn cap_at(mut value: i32, max: i32) -> i32 {
            if value > max {
                value = max;
            }
            value
        }
        
        #[test]
        fn test_cap_at() {
            assert_eq!(cap_at(5, 10), 5);
            assert_eq!(cap_at(10, 10), 10);
            assert_eq!(cap_at(11, 10), 10);
            assert_eq!(cap_at(-3, 10), -3);
            assert_eq!(cap_at(1_000, 0), 0);
        }
        
        // The caller's variable is not affected by the function modifying
        // its parameter. `i32` is `Copy`, so the function got its own copy.
        #[test]
        fn caller_value_is_unchanged() {
            let original = 42;
            let _capped = cap_at(original, 10);
            assert_eq!(original, 42);
        }
        

        Wrapping up functions

        Across these exercises, you used three parts of Rust's function model:

        The central rule is that a function body is a block whose final expression, without a trailing semicolon, becomes the return value. Parameters are bindings too, so they are immutable unless you add mut inside the function.

        Next chapter 6Borrowing and references