Chapter 5

Functions

What do you call a callback that rarely gets called?
Barely functioning.

If you’ve ever gone through customs, you know the importance of declaring what you’re bringing in. Rust takes that idea a step further: you declare both what goes in and what comes out of a function: its parameters and return type.

A function body is a block expression. Its final expression provides the return value when it has no trailing semicolon. That means you can skip the return keyword and just leave off the semicolon on the last expression, which looks pretty nice.

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. Parameter types must be explicit. Rust does not infer the return type from the function body. Omitting the return type means () -- a.k.a. the unit type.

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). Leave the semicolon off the final expression when you want to return its value. 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.

    A recursive call uses ordinary function-call syntax, even inside the function being defined. It needs a base case that returns without calling itself again. Each other call should move closer to that case.

    How does the answer for n relate to the answer for n - 1? Use that relation to decide what each call should return.

    Exercise 2 of 3
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  
      Stuck? Show a hint No spoilers, just a nudge
      1. Which input already has an answer without making another call? Handle it before computing a smaller input, so unsigned subtraction cannot underflow.
      2. Suppose the smaller call has returned its sum. Which term is still missing? Both branches of an if expression must produce the same type.
      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. Any implementation that passes the tests is valid. You don't have to reassign a parameter to solve this.

      Then try this experiment: use the signature fn cap_at(value: i32, max: i32) -> i32, assign max to value when it exceeds the limit, and return value. Run the tests and read the compiler's explanation for rejecting the assignment. Change the parameter binding so this version compiles. The chapter hints can help if you're stuck.

      Look at the second test. The caller's variable is untouched even when the function reassigns 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. In the reassignment experiment, 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);
        }
        
        // Even if the function reassigns its parameter, the caller keeps its original
        // value: `i32` is `Copy`.
        #[test]
        fn caller_value_is_unchanged() {
            let original = 42;
            let _capped = cap_at(original, 10);
            assert_eq!(original, 42);
        }
        

        Wrapping Up Functions

        Here's what each exercise asked you to work out:

        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 Ownership

        Optional Chapters

        Extra practice to explore at your own pace. These chapters do not count toward course progress.

        ← Return to the Rust course curriculum