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). So instead of explaining what a function is, this chapter covers the parts of fn that Rust does its own way: 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, 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 two exercises after it each pull at one more thread: 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 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. The most natural way doesn't compile, because function parameters are immutable bindings by default (like let), so reassigning value is rejected. The fix is one keyword in one place. Read the error, it points right at it.

      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. The moves chapter showed the other half of this: a non-Copy type like String gets moved in instead of copied. The next chapter, borrowing, shows how to lend a value to a function without giving it up at all.

      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

        Three small exercises, three 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 6Borrowing and references