← Fundamentals
Guided lesson 5 questions · 6 languages

Functions as values and higher-order operations

Pass behavior into a function, and get new behavior back.

A function is a value: it can be stored, passed to another function, and returned from one. A function that takes or returns a function is a higher-order function. Writing a function’s name passes the function; writing a call passes its result.

Before you begin

Why this matters

Many functions you use take a closure from you: a sort order, a rule for each item, code to run later. If you can read fn and impl Fn types and tell a function from its result, you can predict what such code does and read the type errors these mix-ups cause.

What this helps you reason about

  • Follow a function you passed in: the function that received it calls it, once per item.
  • Choose between fn(i32) -> i32 and impl Fn(i32) -> i32 for a parameter or a result.
  • Read E0308 when a call was written where a function was expected, or a closure where a function pointer was expected.

A Rust function can take another function or a closure as an argument, and it can return a new closure. This lesson shows how a function uses behavior it receives, how a function builds behavior and returns it, the two ways Rust writes the type of a function, and the mistake of calling a function where the function itself was needed.

Already know this? The short version
  • apply_to_each(&scores, add_bonus) passes add_bonus without calling it, and apply_to_each calls it once per element. A function passed this way is a callback; a function that takes or returns a function is a higher-order function.
  • A factory such as make_scorer(3) runs once and returns a move closure as impl Fn(i32) -> i32; the closure runs each time it is called, after the factory’s frame is gone.
  • fn(i32) -> i32 is a function pointer: named functions and closures that capture nothing. impl Fn(i32) -> i32 also accepts closures that capture; a capturing closure passed as fn gives E0308.
  • f passes the function and f(8) passes its result; the compiler rejects every such mix-up shown here with E0308 or E0277.

Go to the final check· Go to the practice tickets

Words in this lesson (6)
callback
A function passed to another function, which calls it, for example once for each element of an array.
higher-order function
A function that takes a function as an argument, returns a function, or both.
factory
A function whose job is to make and return a new value, such as a new function built from its arguments.
closure
A function without a name, written |parameters| body, together with the variables from the surrounding code that it uses. It borrows those variables, or takes them when written with move.
function pointer
A value of a type such as fn(i32) -> i32: the address of a function with that signature. A named function fits, and so does a closure that captures nothing.
impl Fn
A type written as impl Fn(i32) -> i32: some function or closure that can be called with an i32 and returns an i32. The compiler knows the exact type; the code names only what it can do.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Callbacks

How does a function accept behavior?

Function calls showed that a function is a value: written without parentheses, it is passed, not called. apply_to_each below uses this. It goes through a slice of scores and builds a new Vec, but it does not decide what to do with each score. Its caller passes that in, as the function rule, and apply_to_each calls rule(points) for each score. for &points in scores goes through the slice and copies each i32 into points.

A function passed to another function, which calls it, is called a callback. A function that takes a function as an argument, or returns one, is called a higher-order function. apply_to_each is a higher-order function. add_bonus, which returns points + 2, is an ordinary function, and here it is the callback. With another callback, such as double_points, the same apply_to_each doubles each score.

Sorting works the same way: years.sort_by(by_year) passes a comparison function that decides which of two elements comes first. Ordering, sorting, and comparators follows that callback in detail.

main.rs
fn apply_to_each(scores: &[i32], rule: fn(i32) -> i32) -> Vec<i32> {
    let mut results = Vec::new();
    for &points in scores {
        results.push(rule(points));
    }
    results
}

let results = apply_to_each(&[6, 8, 7], add_bonus);
println!("{:?}", results); // [8, 10, 9]

A higher-order function receives behavior as a function, and decides when to call it.

Coming from JavaScript: A JavaScript callback can be any function. A Rust parameter of type fn(i32) -> i32 accepts only functions and closures that capture nothing; the types section shows what accepts the rest.

Experiment

Follow a rule into the function that calls it

apply_to_each receives the rule as a value and calls it as rule(points), once per score. Which function runs is decided by the caller, in the call to apply_to_each. apply_to_each never names add_bonus or double_points.

Why it matters To change what a higher-order function does with each item, change the function you pass to it. The higher-order function stays the same.

Predict the printed numbers, and which frames are in the frame list while add_bonus runs. Then change apply_to_each(&[6, 8, 7], add_bonus) so that it passes double_points instead, and predict again.

fn apply_to_each(scores: &[i32], rule: fn(i32) -> i32) -> Vec<i32> {
    let mut results = Vec::new();
    for &points in scores {
        results.push(rule(points));
    }
    results
}

fn add_bonus(points: i32) -> i32 {
    points + 2
}

fn double_points(points: i32) -> i32 {
    points * 2
}

fn main() {
    let results = apply_to_each(&[6, 8, 7], add_bonus);
    println!("{:?}", results);
}
What to look for

main::apply_to_each lists rule as fn @ 0x…: the panel shows a function pointer as an address, not as a name. The frame list gives the name: each time line 4 runs rule(points), main::add_bonus appears on top of main::apply_to_each, which waits, on top of main::main. Its frame lists points 6, then 8, then 7, and results grows to [8, 10, 9]. The run prints [8, 10, 9]. After the edit, the frames on top are main::double_points, and the run prints [12, 16, 14]. The code of apply_to_each did not change.

Expected output

[8, 10, 9]

02 / Factories

How does a function return new behavior?

A function can also return a function. make_scorer below takes a difficulty and returns a closure that multiplies points by it. A function whose job is to make and return a new value, here a closure, is often called a factory. The result type impl Fn(i32) -> i32 means “some closure that takes an i32 and returns an i32”: each closure has its own type, which has no name, so the code names only what the result can do. move makes the closure take its own copy of difficulty, as Closures and captured variables showed.

Calling make_scorer(3) runs make_scorer once. It computes no score: it returns the closure, and hard holds it. The closure runs later, each time hard(6) or hard(8) is called, when make_scorer’s frame is gone.

make_scorer(3)(8) does both steps in one expression. The first call returns a closure, and (8) calls it.

main.rs
fn make_scorer(difficulty: i32) -> impl Fn(i32) -> i32 {
    move |points| points * difficulty
}

let hard = make_scorer(3);
println!("{} {}", hard(6), hard(8)); // 18 24

A factory runs once and returns a closure; the closure runs at each call.

Precisely: one type per impl Fn

impl Fn in a result position stands for one concrete type, chosen by the function body. A function that returns one closure in one branch and another closure in a different branch does not compile, because each closure has its own type. Box<dyn Fn(i32) -> i32> is the usual way to return either; a later Rust lesson covers Box.

Experiment

Make a scorer, then call it

Calling make_scorer runs its body once: it makes a new function and returns it. The returned function runs only when it is called, after make_scorer has returned, and it still has difficulty because it captured it. In the edit, make_scorer(3)(8) calls make_scorer again and then calls the function it returns.

Why it matters A factory runs when it is called. The function it returns runs each time that function is called. To use the same rule many times, call the factory once and keep its result.

Predict the two numbers, and whether make_scorer’s frame is in the frame list while the scorer runs. Then change hard(8) to make_scorer(3)(8), and predict: how many times does make_scorer run now?

fn make_scorer(difficulty: i32) -> impl Fn(i32) -> i32 {
    move |points| points * difficulty
}

fn main() {
    let hard = make_scorer(3);
    let first = hard(6);
    let second = hard(8);
    println!("{} {}", first, second);
}
What to look for

main::make_scorer’s frame has difficulty 3. When it returns, main lists hard as {difficulty: 3}: a closure shows as the values it holds. Each call to hard gets a frame named main::make_scorer::{closure#0}, with points and difficulty, directly on top of main::main: main::make_scorer is not in the list. The run prints 18 24. After the edit, main::make_scorer’s frame appears again just before the second closure frame, and the run still prints 18 24.

Expected output

18 24

03 / Types

How do you write the type of a function?

Rust has two ways to write the type of something that can be called. fn(i32) -> i32 is a function pointer: the address of a function that takes an i32 and returns an i32. A named function such as add_bonus fits, and so does a closure that captures nothing, such as |points| points + 2.

A closure that captures a variable does not fit. It needs the captured value as well as the code, and a function pointer has no room for it. Passing one where fn(i32) -> i32 is expected gives:

error[E0308]: mismatched types … expected fn pointer, found closure

with the note “closures can only be coerced to `fn` types if they do not capture any variables”. A parameter written rule: impl Fn(i32) -> i32 accepts both kinds. It is a short form of a type parameter, as in Generics and type parameters: the compiler makes a version of the function for each type it is called with. Fn is one of three closure traits; FnMut and FnOnce, for closures that change or use up what they captured, are the subject of the Rust lesson on closure capture and Fn traits.

main.rs
fn apply_to_each(
    scores: &[i32],
    rule: impl Fn(i32) -> i32,
) -> Vec<i32> {
    let mut results = Vec::new();
    for &points in scores {
        results.push(rule(points));
    }
    results
}

let difficulty = 3;
let results =
    apply_to_each(&[6, 8, 7], |points| points * difficulty);
// [18, 24, 21]; with rule: fn(i32) -> i32, error E0308

fn(i32) -> i32 accepts functions and closures that capture nothing; impl Fn(i32) -> i32 accepts closures that capture too.

Coming from C: A Rust fn(i32) -> i32 is like a C function pointer, and has the same limit: it carries no data. impl Fn is how a Rust callback carries its data, without a separate context pointer.

Experiment

Pass a closure that captures a variable

fn(i32) -> i32 is a function pointer: the address of some code, with no room for captured values. A closure that uses difficulty needs that value as well, so it cannot become a function pointer. impl Fn(i32) -> i32 accepts any function or closure that can be called with an i32 and returns an i32, including one that captures.

Why it matters For a parameter that takes behavior, write impl Fn(i32) -> i32. It accepts named functions and closures that capture; fn(i32) -> i32 accepts only those that capture nothing.

Predict the printed list. Then change rule: impl Fn(i32) -> i32 to rule: fn(i32) -> i32 in apply_to_each, and read the compiler error.

fn apply_to_each(
    scores: &[i32],
    rule: impl Fn(i32) -> i32,
) -> Vec<i32> {
    let mut results = Vec::new();
    for &points in scores {
        results.push(rule(points));
    }
    results
}

fn main() {
    let difficulty = 3;
    let results =
        apply_to_each(&[6, 8, 7], |points| points * difficulty);
    println!("{:?}", results);
}
What to look for

The frame list names the function main::apply_to_each<main::main::{closure_env#0}>: for impl Fn, the compiler makes a version of apply_to_each for this closure’s type, and the part in angle brackets names that type. rule shows {_ref__difficulty: 0x… → 3}: the closure holds a reference to main’s difficulty. Each call rule(points) puts main::main::{closure#0} on top, with points and difficulty 3. The run prints [18, 24, 21]. After the edit, the compiler reports error[E0308]: mismatched types, “expected fn pointer, found closure”, and the note “closures can only be coerced to `fn` types if they do not capture any variables”, under difficulty.

Expected output

[18, 24, 21]

04 / Pass or call

What goes wrong when a function is called instead of passed?

Writing a function’s name passes the function. Writing a call, with parentheses, runs the function at once and passes its result. Mixing the two up is the most common mistake with higher-order functions, and it goes two ways.

Calling too early: describe(add_bonus(8)) runs add_bonus before describe starts and passes the i32 10. The compiler refuses it with error[E0308]: mismatched types, “expected fn pointer, found `i32`”.

Forgetting the call is refused too. println!("{}", add_bonus) gives error[E0277]: `fn(i32) -> i32 {add_bonus}` doesn’t implement `std::fmt::Display`, and the compiler’s help says “use parentheses to call the function”.

main.rs
fn describe(rule: fn(i32) -> i32) -> String {
    format!("8 becomes {}", rule(8))
}

println!("{}", describe(add_bonus)); // 8 becomes 10
// describe(add_bonus(8)): error[E0308]
// println!("{}", add_bonus): error[E0277]

Pass f when the receiver will call it. Write f(x) only where the result itself is needed.

Precisely: {:?} prints a function pointer

A function pointer does implement Debug, so println!("{:?}", rule) with rule: fn(i32) -> i32 compiles and prints an address. The {} form, used for output meant for people, refuses it.

Experiment

Call a function where the function was expected

add_bonus(8) runs add_bonus and gives the i32 10; describe needs a fn(i32) -> i32. The other direction is caught as well: println!("{}", add_bonus) gives error[E0277], “doesn’t implement `std::fmt::Display`”, with the help “use parentheses to call the function”.

Why it matters expected fn pointer, found `i32` means a call was written where the function itself was needed: remove the argument list.

Predict the printed line. Then change describe(add_bonus) to describe(add_bonus(8)), and read what the compiler says.

fn describe(rule: fn(i32) -> i32) -> String {
    format!("8 becomes {}", rule(8))
}

fn add_bonus(points: i32) -> i32 {
    points + 2
}

fn main() {
    println!("{}", describe(add_bonus));
}
What to look for

main::describe lists rule as fn @ 0x…, and the call rule(8) puts main::add_bonus on top of it, with points 8. The run prints 8 becomes 10. After the edit, the compiler reports error[E0308]: mismatched types, “expected fn pointer, found `i32`”, with the note expected fn pointer `fn(i32) -> i32`, found type `i32`.

Expected output

8 becomes 10

05 / Try it

Can you follow behavior passed in and returned?

One function is passed, one is built by a factory, and one call returns a closure. Decide when each one runs.

main.rs
fn apply_twice(rule: impl Fn(i32) -> i32, points: i32) -> i32 {
    rule(rule(points))
}

fn make_scorer(difficulty: i32) -> impl Fn(i32) -> i32 {
    move |points| points * difficulty
}

fn add_bonus(points: i32) -> i32 {
    points + 2
}

fn main() {
    let a = apply_twice(add_bonus, 5);
    let b = apply_twice(make_scorer(2), 3);
    println!("{} {}", a, b);
}

What is printed?

Reveal the reasoning

9 12. apply_twice calls the rule it receives two times, the second time on the first result. add_bonus turns 5 into 7, then 9. make_scorer(2) runs once, before apply_twice starts, and its result is a function that doubles: 3 becomes 6, then 12.

Practice tickets

  1. HOF-101 · The board shows each athlete’s worst result as their best → Free
  2. HOF-102 · Every win counts as a first-round win → Sign in · Pro
  3. HOF-103 · Practice marks fall below zero or stay above the scale → Sign in · Pro

Keep exploring

Stack and heap →

Primary references