Contents

Ch.13 — Functional Language Features: Iterators and Closures

Chapter 13

Functional Language Features: Iterators and Closures


Rust’s design has taken inspiration from many existing languages and techniques, and one significant influence is functional programming. Programming in a functional style often includes using functions as values by passing them in arguments, returning them from other functions, assigning them to variables for later execution, and so forth.

  • 13.1 Closures
  • 13.2 Processing a Series of Items with Iterators
  • 13.3 Improving Our I/O Project
  • 13.4 Performance in Loops vs. Iterators

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

1

Chapter 13 · Introduction

What this chapter covers

Definition 13.1 (Functional style). Programming that uses functions as values: passing them in arguments, returning them from other functions, assigning them to variables for later execution, and so forth.

This chapter won’t debate what functional programming is or isn’t; it discusses some features of Rust that are similar to features in many languages often referred to as functional. More specifically, it covers:

  • Closures, a function-like construct you can store in a variable;
  • Iterators, a way of processing a series of elements;
  • how to use closures and iterators to improve the I/O project in Chapter 12;
  • the performance of closures and iterators (spoiler alert: they’re faster than you might think!).

Other Rust features covered earlier, such as pattern matching and enums, are also influenced by the functional style. Because mastering closures and iterators is an important part of writing fast, idiomatic Rust code, this entire chapter is devoted to them.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

2

Chapter 13 · 13.1 Closures

Closures, and capturing the environment

Definition 13.2 (Closure). An anonymous function you can save in a variable or pass as an argument to other functions. Unlike functions, closures can capture values from the scope in which they’re defined.

You can create a closure in one place and then call it elsewhere to evaluate it in a different context. These features allow for code reuse and behavior customization.

Capturing the environment

First, how closures capture values from the environment they’re defined in for later use. The scenario: every so often, a T-shirt company gives away an exclusive, limited-edition shirt to someone on its mailing list as a promotion. People on the mailing list can optionally add their favorite color to their profile. If the person chosen for a free shirt has a favorite color set, they get that color; if not, they get whatever color the company currently has the most of.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

3

Chapter 13 · 13.1 Closures

The shirt giveaway setup

There are many ways to implement this. Here, an enum ShirtColor has the variants Red and Blue (limiting the colors for simplicity). The company’s inventory is an Inventory struct with a field shirts holding a Vec<ShirtColor> of the shirt colors currently in stock. The method giveaway defined on Inventory gets the optional shirt color preference of the free-shirt winner and returns the shirt color the person will get.

Listing 13.1: Shirt company giveaway situation — src/main.rs

#[derive(Debug, PartialEq, Copy, Clone)]

enum ShirtColor {

    Red,

    Blue,

}

 

struct Inventory {

    shirts: Vec<ShirtColor>,

}

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

4

Chapter 13 · 13.1 Closures

The giveaway and most_stocked methods

Listing 13.1 (continued): Shirt company giveaway situation — src/main.rs

impl Inventory {

    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {

        user_preference.unwrap_or_else(|| self.most_stocked())

    }

 

    fn most_stocked(&self) -> ShirtColor {

        let mut num_red = 0;

        let mut num_blue = 0;

 

        for color in &self.shirts {

            match color {

                ShirtColor::Red => num_red += 1,

                ShirtColor::Blue => num_blue += 1,

            }

        }

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

5

Chapter 13 · 13.1 Closures

Picking the most-stocked color; the store in main

Listing 13.1 (continued): Shirt company giveaway situation — src/main.rs

        if num_red > num_blue {

            ShirtColor::Red

        } else {

            ShirtColor::Blue

        }

    }

}

 

fn main() {

    let store = Inventory {

        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],

    };

The store defined in main has two blue shirts and one red shirt remaining to distribute for this limited-edition promotion. Next, main calls the giveaway method for a user with a preference for a red shirt and for a user without any preference.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

6

Chapter 13 · 13.1 Closures

Two giveaways: with and without a preference

Listing 13.1 (continued): Shirt company giveaway situation — src/main.rs

 

    let user_pref1 = Some(ShirtColor::Red);

    let giveaway1 = store.giveaway(user_pref1);

    println!(

        "The user with preference {:?} gets {:?}",

        user_pref1, giveaway1

    );

 

    let user_pref2 = None;

    let giveaway2 = store.giveaway(user_pref2);

    println!(

        "The user with preference {:?} gets {:?}",

        user_pref2, giveaway2

    );

}

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

7

Chapter 13 · 13.1 Closures

How giveaway uses a closure

The code sticks to concepts already learned, except for the body of giveaway, which uses a closure. It gets the user preference as a parameter of type Option<ShirtColor> and calls unwrap_or_else on user_preference. The standard library’s unwrap_or_else method on Option<T> takes one argument: a closure without any arguments that returns a value T (the type stored in the Some variant, here ShirtColor).

  • If the Option<T> is Some, unwrap_or_else returns the value from within the Some.
  • If it is None, unwrap_or_else calls the closure and returns the value returned by the closure.

user_preference.unwrap_or_else(|| self.most_stocked())

The closure expression || self.most_stocked() takes no parameters (if it had any, they would appear between the two vertical pipes). Its body calls self.most_stocked(). The closure is defined here, and the implementation of unwrap_or_else will evaluate it later if the result is needed.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

8

Chapter 13 · 13.1 Closures

Running the giveaway: the closure captures self

Running this code prints the following:

$ cargo run

   Compiling shirt-company v0.1.0 (file:///projects/shirt-company)

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s

     Running `target/debug/shirt-company`

The user with preference Some(Red) gets Red

The user with preference None gets Blue

One interesting aspect: we passed a closure that calls self.most_stocked() on the current Inventory instance. The standard library didn’t need to know anything about the Inventory or ShirtColor types we defined, or the logic we want to use in this scenario.

The closure captures an immutable reference to the self Inventory instance and passes it, with the code we specify, to the unwrap_or_else method. Functions, on the other hand, are not able to capture their environment in this way.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

9

Chapter 13 · 13.1 Closures

Inferring and annotating closure types

Closures don’t usually require you to annotate the types of the parameters or the return value like fn functions do. Type annotations are required on functions because the types are part of an explicit interface exposed to your users; defining this interface rigidly ensures that everyone agrees on what types of values a function uses and returns. Closures aren’t used in an exposed interface like this: they’re stored in variables and used without naming them and exposing them to users of our library.

Closures are typically short and relevant only within a narrow context rather than in any arbitrary scenario. Within these limited contexts, the compiler can infer the types of the parameters and the return type, similar to how it infers the types of most variables (there are rare cases where the compiler needs closure type annotations too).

As with variables, we can add type annotations to increase explicitness and clarity, at the cost of being more verbose than strictly necessary. Listing 13.2 annotates a closure; this time the closure is defined and stored in a variable rather than defined in the spot where it is passed as an argument, as in Listing 13.1.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

10

Chapter 13 · 13.1 Closures

A closure with annotated types

Listing 13.2: Adding optional type annotations of the parameter and return value types in the closure — src/main.rs

use std::thread;

use std::time::Duration;

 

fn generate_workout(intensity: u32, random_number: u32) {

    let expensive_closure = |num: u32| -> u32 {

        println!("calculating slowly...");

        thread::sleep(Duration::from_secs(2));

        num

    };

 

    if intensity < 25 {

        println!("Today, do {} pushups!", expensive_closure(intensity));

        println!("Next, do {} situps!", expensive_closure(intensity));

    } else {

        if random_number == 3 {

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

11

Chapter 13 · 13.1 Closures

Calling the stored closure

Listing 13.2 (continued): Adding optional type annotations in the closure — src/main.rs

            println!("Take a break today! Remember to stay hydrated!");

        } else {

            println!(

                "Today, run for {} minutes!",

                expensive_closure(intensity)

            );

        }

    }

}

 

fn main() {

    let simulated_user_specified_value = 10;

    let simulated_random_number = 7;

 

    generate_workout(simulated_user_specified_value, simulated_random_number);

}

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

12

Chapter 13 · 13.1 Closures

Closure syntax compared with function syntax

With type annotations added, the syntax of closures looks more similar to the syntax of functions. Closure syntax is like function syntax except for the use of pipes and the amount of syntax that is optional.

Example 13.3. A function that adds 1 to its parameter, and closures with the same behavior. Spaces were added to line up the relevant parts.

fn  add_one_v1   (x: u32) -> u32 { x + 1 }

let add_one_v2 = |x: u32| -> u32 { x + 1 };

let add_one_v3 = |x|             { x + 1 };

let add_one_v4 = |x|               x + 1  ;

The first line is a function definition; the second, a fully annotated closure definition. The third removes the type annotations from the closure definition. The fourth removes the brackets, which are optional because the closure body has only one expression.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

13

Chapter 13 · 13.1 Closures

One concrete type per closure parameter

All four are valid definitions that produce the same behavior when called. The add_one_v3 and add_one_v4 lines require the closures to be evaluated to compile, because the types are inferred from their usage — just as let v = Vec::new(); needs type annotations or values of some type inserted into the Vec for Rust to infer the type.

For closure definitions, the compiler infers one concrete type for each parameter and for the return value. Listing 13.3 defines a short closure that just returns the value it receives — not very useful except for this example. With no type annotations, we can call it with any type, here String the first time; calling example_closure with an integer afterwards is an error.

Listing 13.3: Attempting to call a closure whose types are inferred with two different types — src/main.rs

fn main() {

    let example_closure = |x| x;

 

    let s = example_closure(String::from("hello"));

    let n = example_closure(5);

}

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

14

Chapter 13 · 13.1 Closures

The compiler’s error: mismatched types

The compiler gives us this error:

$ cargo run

   Compiling closure-example v0.1.0 (file:///projects/closure-example)

error[E0308]: mismatched types

 --> src/main.rs:5:29

  |

5 |     let n = example_closure(5);

  |             --------------- ^ expected `String`, found integer

  |             |

  |             arguments to this function are incorrect

  |

note: expected because the closure was earlier called with an argument of type `String`

 --> src/main.rs:4:29

  |

4 |     let s = example_closure(String::from("hello"));

  |             --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`

  |             |

  |             in this closure call

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

15

Chapter 13 · 13.1 Closures

The first call locks in the types

note: closure parameter defined here

 --> src/main.rs:2:28

  |

2 |     let example_closure = |x| x;

  |                            ^

help: try using a conversion method

  |

5 |     let n = example_closure(5.to_string());

  |                              ++++++++++++

 

For more information about this error, try `rustc --explain E0308`.

error: could not compile `closure-example` (bin "closure-example") due to 1 previous error

The first time we call example_closure with the String value, the compiler infers the type of x and the return type of the closure to be String. Those types are then locked into the closure in example_closure, and we get a type error when we next try to use a different type with the same closure.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

16

Chapter 13 · 13.1 Closures

Capturing references or moving ownership

Closures can capture values from their environment in three ways, mapping directly to the three ways a function can take a parameter: borrowing immutably, borrowing mutably, and taking ownership. The closure decides which to use based on what its body does with the captured values. In Listing 13.4 it captures an immutable reference to the vector list, because that is all it needs to print the value.

Listing 13.4: Defining and calling a closure that captures an immutable reference — src/main.rs

fn main() {

    let list = vec![1, 2, 3];

    println!("Before defining closure: {list:?}");

 

    let only_borrows = || println!("From closure: {list:?}");

 

    println!("Before calling closure: {list:?}");

    only_borrows();

    println!("After calling closure: {list:?}");

}

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

17

Chapter 13 · 13.1 Closures

Many immutable borrows at once

This example also shows that a variable can bind to a closure definition, and we can later call the closure by using the variable name and parentheses as if the variable name were a function name.

Because we can have multiple immutable references to list at the same time, list is still accessible before the closure definition, after the definition but before the call, and after the call. This code compiles, runs, and prints:

$ cargo run

   Compiling closure-example v0.1.0 (file:///projects/closure-example)

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s

     Running `target/debug/closure-example`

Before defining closure: [1, 2, 3]

Before calling closure: [1, 2, 3]

From closure: [1, 2, 3]

After calling closure: [1, 2, 3]

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

18

Chapter 13 · 13.1 Closures

Capturing a mutable reference

In Listing 13.5, the closure body adds an element to the list vector. The closure now captures a mutable reference.

Listing 13.5: Defining and calling a closure that captures a mutable reference — src/main.rs

fn main() {

    let mut list = vec![1, 2, 3];

    println!("Before defining closure: {list:?}");

 

    let mut borrows_mutably = || list.push(7);

 

    borrows_mutably();

    println!("After calling closure: {list:?}");

}

This code compiles, runs, and prints the output on the next slide.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

19

Chapter 13 · 13.1 Closures

When the mutable borrow ends

$ cargo run

   Compiling closure-example v0.1.0 (file:///projects/closure-example)

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s

     Running `target/debug/closure-example`

Before defining closure: [1, 2, 3]

After calling closure: [1, 2, 3, 7]

There’s no longer a println! between the definition and the call of borrows_mutably: when it is defined, it captures a mutable reference to list. We don’t use the closure again after calling it, so the mutable borrow ends there. Between the definition and the call, an immutable borrow to print isn’t allowed, because no other borrows are allowed when there’s a mutable borrow. Try adding a println! there to see what error message you get!

To force the closure to take ownership of the values it uses in the environment, even though the body doesn’t strictly need ownership, use the move keyword before the parameter list.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

20

Chapter 13 · 13.1 Closures

Forcing ownership with move

This technique is mostly useful when passing a closure to a new thread, to move the data so that it’s owned by the new thread. Chapter 16 discusses threads and why you would want them in detail; for now, here is a brief look at spawning a new thread using a closure that needs the move keyword. Listing 13.6 modifies Listing 13.4 to print the vector in a new thread rather than in the main thread.

Listing 13.6: Using move to force the closure for the thread to take ownership of list — src/main.rs

use std::thread;

 

fn main() {

    let list = vec![1, 2, 3];

    println!("Before defining closure: {list:?}");

 

    thread::spawn(move || println!("From thread: {list:?}"))

        .join()

        .unwrap();

}

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

21

Chapter 13 · 13.1 Closures

Why the thread’s closure needs move

We spawn a new thread, giving it a closure to run as an argument; the closure body prints out the list.

  • In Listing 13.4, the closure only captured list using an immutable reference, because that’s the least amount of access to list needed to print it.
  • Here, even though the closure body still only needs an immutable reference, we must specify that list be moved into the closure, by putting move at the beginning of the closure definition.
  • If the main thread performed more operations before calling join on the new thread, the new thread might finish before the rest of the main thread finishes, or the main thread might finish first.
  • If the main thread kept ownership of list but ended before the new thread and dropped list, the immutable reference in the thread would be invalid. Therefore, the compiler requires that list be moved into the closure given to the new thread so that the reference will be valid.

Try removing the move keyword, or using list in the main thread after the closure is defined, to see what compiler errors you get!

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

22

Chapter 13 · 13.1 Closures

Moving captured values out of closures

Once a closure has captured a reference or captured ownership of a value from the environment where it is defined (affecting what, if anything, is moved into the closure), the code in its body defines what happens to the references or values when the closure is evaluated later (affecting what, if anything, is moved out of the closure).

A closure body can do any of the following:

  • move a captured value out of the closure;
  • mutate the captured value;
  • neither move nor mutate the value;
  • capture nothing from the environment to begin with.

How a closure captures and handles values from the environment affects which traits it implements, and traits are how functions and structs specify what kinds of closures they can use. Closures automatically implement one, two, or all three of the Fn traits, in an additive fashion, depending on how the body handles the values.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

23

Chapter 13 · 13.1 Closures

The three Fn traits

Definition 13.4 (FnOnce). Applies to closures that can be called once. All closures implement at least this trait, because all closures can be called. A closure that moves captured values out of its body implements only FnOnce and none of the other Fn traits, because it can only be called once.

Definition 13.5 (FnMut). Applies to closures that don’t move captured values out of their body but might mutate the captured values. These closures can be called more than once.

Definition 13.6 (Fn). Applies to closures that don’t move captured values out of their body and don’t mutate captured values, as well as closures that capture nothing from their environment. These closures can be called more than once without mutating their environment, which is important in cases such as calling a closure multiple times concurrently.

The traits are additive: every Fn closure is also FnMut, and every FnMut closure is also FnOnce.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

24

Chapter 13 · 13.1 Closures

The definition of unwrap_or_else

Here is the definition of the unwrap_or_else method on Option<T> used in Listing 13.1:

impl<T> Option<T> {

    pub fn unwrap_or_else<F>(self, f: F) -> T

    where

        F: FnOnce() -> T

    {

        match self {

            Some(x) => x,

            None => f(),

        }

    }

}

Recall that T is the generic type representing the type of the value in the Some variant of an Option. That type T is also the return type of unwrap_or_else: code that calls unwrap_or_else on an Option<String>, for example, will get a String.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

25

Chapter 13 · 13.1 Closures

Reading the FnOnce trait bound

Next, notice that unwrap_or_else has the additional generic type parameter F. F is the type of the parameter named f, which is the closure we provide when calling unwrap_or_else.

The trait bound on F is FnOnce() -> T: F must be able to be called once, take no arguments, and return a T. Using FnOnce in the bound expresses the constraint that unwrap_or_else will not call f more than once. In its body, if the Option is Some, f won’t be called; if it is None, f is called once. Because all closures implement FnOnce, unwrap_or_else accepts all three kinds of closures and is as flexible as it can be.

Remark. If what we want to do doesn’t require capturing a value from the environment, we can use the name of a function rather than a closure where we need something that implements one of the Fn traits. For example, on an Option<Vec<T>> value, we could call unwrap_or_else(Vec::new) to get a new, empty vector if the value is None. The compiler automatically implements whichever of the Fn traits is applicable for a function definition.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

26

Chapter 13 · 13.1 Closures

sort_by_key takes an FnMut closure

The standard library method sort_by_key, defined on slices, shows how its bound differs from that of unwrap_or_else, and why it uses FnMut instead of FnOnce.

The closure gets one argument, a reference to the current item in the slice being considered, and returns a value of type K that can be ordered. This is useful when you want to sort a slice by a particular attribute of each item.

In Listing 13.7, a list of Rectangle instances is ordered with sort_by_key by their width attribute, from low to high.

Listing 13.7: Using sort_by_key to order rectangles by width — src/main.rs

#[derive(Debug)]

struct Rectangle {

    width: u32,

    height: u32,

}

 

fn main() {

    let mut list = [

        Rectangle { width: 10, height: 1 },

        Rectangle { width: 3, height: 5 },

        Rectangle { width: 7, height: 12 },

    ];

 

    list.sort_by_key(|r| r.width);

    println!("{list:#?}");

}

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

27

Chapter 13 · 13.1 Closures

Listing 13.7 prints the rectangles sorted by width

$ cargo run

   Compiling rectangles v0.1.0 (file:///projects/rectangles)

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s

     Running `target/debug/rectangles`

[

    Rectangle {

        width: 3,

        height: 5,

    },

    Rectangle {

        width: 7,

        height: 12,

    },

    Rectangle {

        width: 10,

        height: 1,

    },

]

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

28

Chapter 13 · 13.1 Closures

An FnOnce closure that sort_by_key rejects

sort_by_key is defined to take an FnMut closure because it calls the closure multiple times: once for each item in the slice. The closure |r| r.width doesn’t capture, mutate, or move anything out from its environment, so it meets the trait bound requirements.

In contrast, Listing 13.8 shows a closure that implements just the FnOnce trait, because it moves a value out of the environment. The compiler won’t let us use this closure with sort_by_key.

Listing 13.8: Attempting to use an FnOnce closure with sort_by_key — src/main.rs

#[derive(Debug)]

struct Rectangle {

    width: u32,

    height: u32,

}

 

fn main() {

    let mut list = [

        Rectangle { width: 10, height: 1 },

        Rectangle { width: 3, height: 5 },

        Rectangle { width: 7, height: 12 },

    ];

 

    let mut sort_operations = vec![];

    let value = String::from("closure called");

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

29

Chapter 13 · 13.1 Closures

Moving value out makes the closure FnOnce

Listing 13.8 (continued): Attempting to use an FnOnce closure with sort_by_key — src/main.rs

    list.sort_by_key(|r| {

        sort_operations.push(value);

        r.width

    });

    println!("{list:#?}");

}

This is a contrived, convoluted way (that doesn’t work) to try to count the number of times sort_by_key calls the closure when sorting list. It attempts the counting by pushing value — a String from the closure’s environment — into the sort_operations vector. The closure captures value and then moves it out of the closure by transferring ownership of value to the sort_operations vector.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

30

Chapter 13 · 13.1 Closures

The error: cannot move out of value

This closure can be called once; trying to call it a second time wouldn’t work, because value would no longer be in the environment to be pushed into sort_operations again! Therefore, this closure only implements FnOnce. Compiling gives this error: value can’t be moved out of the closure because the closure must implement FnMut.

$ cargo run

   Compiling rectangles v0.1.0 (file:///projects/rectangles)

error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure

  --> src/main.rs:18:30

   |

15 |     let value = String::from("closure called");

   |         -----   ------------------------------ move occurs because `value` has type `String`,

                                                    which does not implement the `Copy` trait

   |         |

   |         captured outer variable

16 |

17 |     list.sort_by_key(|r| {

   |                      --- captured by this `FnMut` closure

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

31

Chapter 13 · 13.1 Closures

Where the value is moved, and the compiler’s hints

18 |         sort_operations.push(value);

   |                              ^^^^^ `value` is moved here

   |

help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times,

      but `FnOnce` closures may consume them only once

  --> /rustc/2d8144b7880597b6e6d3dfd63a9a9efae3f533d3/library/alloc/src/slice.rs:249:11

help: consider cloning the value if the performance cost is acceptable

   |

18 |         sort_operations.push(value.clone());

   |                                   ++++++++

 

For more information about this error, try `rustc --explain E0507`.

error: could not compile `rectangles` (bin "rectangles") due to 1 previous error

The error points to the line in the closure body that moves value out of the environment. To fix this, we need to change the closure body so that it doesn’t move values out of the environment.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

32

Chapter 13 · 13.1 Closures

Counting with an FnMut closure instead

Keeping a counter in the environment and incrementing its value in the closure body is a more straightforward way to count the number of times the closure is called. The closure in Listing 13.9 works with sort_by_key because it only captures a mutable reference to the num_sort_operations counter and can therefore be called more than once.

Listing 13.9: Using an FnMut closure with sort_by_key is allowed. — src/main.rs

#[derive(Debug)]

struct Rectangle {

    width: u32,

    height: u32,

}

 

fn main() {

    let mut list = [

        Rectangle { width: 10, height: 1 },

        Rectangle { width: 3, height: 5 },

        Rectangle { width: 7, height: 12 },

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

33

Chapter 13 · 13.1 Closures

Incrementing a captured counter

Listing 13.9 (continued): Using an FnMut closure with sort_by_key is allowed. — src/main.rs

    ];

 

    let mut num_sort_operations = 0;

    list.sort_by_key(|r| {

        num_sort_operations += 1;

        r.width

    });

    println!("{list:#?}, sorted in {num_sort_operations} operations");

}

The Fn traits are important when defining or using functions or types that make use of closures. The next section discusses iterators. Many iterator methods take closure arguments, so keep these closure details in mind!

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

34

Chapter 13 · 13.2 Processing a Series of Items with Iterators

The iterator pattern, and laziness

The iterator pattern allows you to perform some task on a sequence of items in turn. An iterator is responsible for the logic of iterating over each item and determining when the sequence has finished. When you use iterators, you don’t have to reimplement that logic yourself.

Definition 13.7 (Lazy). In Rust, iterators are lazy: they have no effect until you call methods that consume the iterator to use it up.

For example, Listing 13.10 creates an iterator over the items in the vector v1 by calling the iter method defined on Vec<T>. This code by itself doesn’t do anything useful.

Listing 13.10: Creating an iterator — src/main.rs

fn main() {

    let v1 = vec![1, 2, 3];

 

    let v1_iter = v1.iter();

}

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

35

Chapter 13 · 13.2 Processing a Series of Items with Iterators

Using an iterator in a for loop

The iterator is stored in the v1_iter variable. Once created, an iterator can be used in a variety of ways. Listing 3.5 iterated over an array using a for loop to execute some code on each item. Under the hood, this implicitly created and then consumed an iterator, but how exactly that works was glossed over until now.

Listing 13.11 separates the creation of the iterator from its use in the for loop. When the loop is called using the iterator in v1_iter, each element in the iterator is used in one iteration of the loop, which prints out each value.

Listing 13.11: Using an iterator in a for loop — src/main.rs

fn main() {

    let v1 = vec![1, 2, 3];

 

    let v1_iter = v1.iter();

 

    for val in v1_iter {

        println!("Got: {val}");

    }

}

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

36

Chapter 13 · 13.2 Processing a Series of Items with Iterators

What iterators save you from writing

In languages whose standard libraries don’t provide iterators, you would likely write this same functionality by:

  1. starting a variable at index 0;
  2. using that variable to index into the vector to get a value;
  3. incrementing the variable value in a loop until it reached the total number of items in the vector.

Iterators handle all of that logic for you, cutting down on repetitive code you could potentially mess up. Iterators also give you more flexibility to use the same logic with many different kinds of sequences, not just data structures you can index into, like vectors. The next slides examine how iterators do that.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

37

Chapter 13 · 13.2 Processing a Series of Items with Iterators

The Iterator trait and the next method

All iterators implement a trait named Iterator that is defined in the standard library. Its definition looks like this:

pub trait Iterator {

    type Item;

 

    fn next(&mut self) -> Option<Self::Item>;

 

    // methods with default implementations elided

}

Definition 13.8 (Associated type). The new syntax type Item and Self::Item defines an associated type with this trait (covered in depth in Chapter 20). Implementing Iterator requires that you also define an Item type, which is used in the return type of next: the Item type is the type returned from the iterator.

The Iterator trait only requires implementors to define one method: next, which returns one item of the iterator at a time, wrapped in Some, and, when iteration is over, returns None.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

38

Chapter 13 · 13.2 Processing a Series of Items with Iterators

Calling next directly consumes the iterator

We can call next on iterators directly. Listing 13.12 shows the values returned from repeated calls to next on the iterator created from the vector.

v1_iter must be mutable: calling next changes internal state that the iterator uses to keep track of where it is in the sequence. In other words, this code consumes, or uses up, the iterator; each call to next eats up an item. With a for loop, v1_iter didn’t need to be mutable, because the loop took ownership of it and made it mutable behind the scenes.

Listing 13.12: Calling the next method on an iterator — src/lib.rs

#[cfg(test)]

mod tests {

    #[test]

    fn iterator_demonstration() {

        let v1 = vec![1, 2, 3];

 

        let mut v1_iter = v1.iter();

 

        assert_eq!(v1_iter.next(), Some(&1));

        assert_eq!(v1_iter.next(), Some(&2));

        assert_eq!(v1_iter.next(), Some(&3));

        assert_eq!(v1_iter.next(), None);

    }

}

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

39

Chapter 13 · 13.2 Processing a Series of Items with Iterators

iter, into_iter and iter_mut

The values we get from the calls to next in Listing 13.12 are immutable references to the values in the vector: the iter method produces an iterator over immutable references. To create an iterator that takes ownership of v1 and returns owned values, call into_iter instead of iter. Similarly, to iterate over mutable references, call iter_mut instead of iter.

Table 13.1: Three ways to get an iterator over a collection

MethodItems the iterator produces
iterImmutable references to the values
into_iterOwned values (the iterator takes ownership of the collection)
iter_mutMutable references to the values

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

40

Chapter 13 · 13.2 Processing a Series of Items with Iterators

Methods that consume the iterator

The Iterator trait has a number of different methods with default implementations provided by the standard library; you can find out about them in the standard library API documentation for the Iterator trait. Some of these methods call the next method in their definition, which is why you’re required to implement next when implementing the Iterator trait.

Definition 13.9 (Consuming adapter). A method that calls next, so that calling it uses up the iterator.

One example is the sum method, which takes ownership of the iterator and iterates through the items by repeatedly calling next, thus consuming the iterator. As it iterates through, it adds each item to a running total and returns the total when iteration is complete. Listing 13.13 has a test illustrating a use of sum.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

41

Chapter 13 · 13.2 Processing a Series of Items with Iterators

Summing the items with sum

Listing 13.13: Calling the sum method to get the total of all items in the iterator — src/lib.rs

#[cfg(test)]

mod tests {

    #[test]

    fn iterator_sum() {

        let v1 = vec![1, 2, 3];

 

        let v1_iter = v1.iter();

 

        let total: i32 = v1_iter.sum();

 

        assert_eq!(total, 6);

    }

}

We aren’t allowed to use v1_iter after the call to sum, because sum takes ownership of the iterator we call it on.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

42

Chapter 13 · 13.2 Processing a Series of Items with Iterators

Methods that produce other iterators: map

Definition 13.10 (Iterator adapter). A method defined on the Iterator trait that doesn’t consume the iterator; instead, it produces a different iterator by changing some aspect of the original iterator.

Listing 13.14 calls the iterator adapter method map, which takes a closure to call on each item as the items are iterated through. map returns a new iterator that produces the modified items. The closure here creates a new iterator in which each item from the vector is incremented by 1.

Listing 13.14: Calling the iterator adapter map to create a new iterator — src/main.rs

fn main() {

    let v1: Vec<i32> = vec![1, 2, 3];

 

    v1.iter().map(|x| x + 1);

}

However, this code produces a warning (next slide).

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

43

Chapter 13 · 13.2 Processing a Series of Items with Iterators

Warning: iterators are lazy

$ cargo run

   Compiling iterators v0.1.0 (file:///projects/iterators)

warning: unused `Map` that must be used

 --> src/main.rs:4:5

  |

4 |     v1.iter().map(|x| x + 1);

  |     ^^^^^^^^^^^^^^^^^^^^^^^^

  |

  = note: iterators are lazy and do nothing unless consumed

  = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default

help: use `let _ = ...` to ignore the resulting value

  |

4 |     let _ = v1.iter().map(|x| x + 1);

  |     +++++++

 

warning: `iterators` (bin "iterators") generated 1 warning

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s

     Running `target/debug/iterators`

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

44

Chapter 13 · 13.2 Processing a Series of Items with Iterators

Consuming the adapted iterator with collect

The code in Listing 13.14 doesn’t do anything; the closure we’ve specified never gets called. The warning reminds us why: iterator adapters are lazy, and we need to consume the iterator here.

To fix this, use the collect method (used with env::args in Listing 12.1), which consumes the iterator and collects the resultant values into a collection data type. In Listing 13.15, the results of iterating over the iterator returned from map are collected into a vector, which ends up containing each item from the original vector, incremented by 1.

Listing 13.15: Calling the map method to create a new iterator, and then calling the collect method to consume the new iterator and create a vector — src/main.rs

fn main() {

    let v1: Vec<i32> = vec![1, 2, 3];

 

    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

 

    assert_eq!(v2, vec![2, 3, 4]);

}

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

45

Chapter 13 · 13.2 Processing a Series of Items with Iterators

Chaining adapters; closures that capture

Because map takes a closure, we can specify any operation we want to perform on each item. This is a great example of how closures let you customize some behavior while reusing the iteration behavior that the Iterator trait provides.

You can chain multiple calls to iterator adapters to perform complex actions in a readable way. But because all iterators are lazy, you have to call one of the consuming adapter methods to get results from calls to iterator adapters.

Closures that capture their environment

Many iterator adapters take closures as arguments, and commonly those closures capture their environment. Take the filter method: its closure gets an item from the iterator and returns a bool. If the closure returns true, the value is included in the iterator produced by filter; if it returns false, the value isn’t included.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

46

Chapter 13 · 13.2 Processing a Series of Items with Iterators

filter with a closure that captures shoe_size

In Listing 13.16, filter is used with a closure that captures the shoe_size variable from its environment to iterate over a collection of Shoe struct instances. It returns only shoes that are the specified size.

Listing 13.16: Using the filter method with a closure that captures shoe_size — src/lib.rs

#[derive(PartialEq, Debug)]

struct Shoe {

    size: u32,

    style: String,

}

 

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {

    shoes.into_iter().filter(|s| s.size == shoe_size).collect()

}

 

#[cfg(test)]

mod tests {

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

47

Chapter 13 · 13.2 Processing a Series of Items with Iterators

Testing shoes_in_size

Listing 13.16 (continued): Using the filter method with a closure that captures shoe_size — src/lib.rs

    use super::*;

 

    #[test]

    fn filters_by_size() {

        let shoes = vec![

            Shoe {

                size: 10,

                style: String::from("sneaker"),

            },

            Shoe {

                size: 13,

                style: String::from("sandal"),

            },

            Shoe {

                size: 10,

                style: String::from("boot"),

            },

        ];

 

        let in_my_size = shoes_in_size(shoes, 10);

 

        assert_eq!(

            in_my_size,

            vec![

                Shoe {

                    size: 10,

                    style: String::from("sneaker")

                },

                Shoe {

                    size: 10,

                    style: String::from("boot")

                },

            ]

        );

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

48

Chapter 13 · 13.2 Processing a Series of Items with Iterators

How shoes_in_size works

Listing 13.16 (continued): Using the filter method with a closure that captures shoe_size — src/lib.rs

    }

}

  1. shoes_in_size takes ownership of a vector of shoes and a shoe size as parameters, and returns a vector containing only shoes of the specified size.
  2. In its body, into_iter creates an iterator that takes ownership of the vector. Then filter adapts that iterator into a new iterator that only contains elements for which the closure returns true.
  3. The closure captures the shoe_size parameter from the environment and compares it with each shoe’s size, keeping only shoes of the size specified.
  4. Finally, collect gathers the values returned by the adapted iterator into a vector that’s returned by the function.

The test shows that when we call shoes_in_size, we get back only shoes that have the same size as the value we specified.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

49

Chapter 13 · 13.3 Improving Our I/O Project

Removing a clone using an iterator

With this new knowledge about iterators, the I/O project from Chapter 12 can use iterators to make places in the code clearer and more concise — in the Config::build function and the search function.

In Listing 12.6, code took a slice of String values and created an instance of the Config struct by indexing into the slice and cloning the values, allowing the Config struct to own those values. Listing 13.17 reproduces the implementation of Config::build as it was in Listing 12.23.

Listing 13.17: Reproduction of the Config::build function from Listing 12.23 — src/main.rs

impl Config {

    fn build(args: &[String]) -> Result<Config, &'static str> {

        if args.len() < 3 {

            return Err("not enough arguments");

        }

 

        let query = args[1].clone();

        let file_path = args[2].clone();

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

50

Chapter 13 · 13.3 Improving Our I/O Project

The clones we promised to remove

Listing 13.17 (continued): Reproduction of the Config::build function from Listing 12.23 — src/main.rs

        let ignore_case = env::var("IGNORE_CASE").is_ok();

 

        Ok(Config {

            query,

            file_path,

            ignore_case,

        })

    }

}

At the time, we said not to worry about the inefficient clone calls because we would remove them in the future. Well, that time is now!

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

51

Chapter 13 · 13.3 Improving Our I/O Project

Why clone was needed, and the plan

We needed clone because the parameter args is a slice with String elements, but the build function doesn’t own args. To return ownership of a Config instance, we had to clone the values for the query and file_path fields so that the Config instance can own its values.

Now build can take ownership of an iterator as its argument instead of borrowing a slice. We’ll use the iterator functionality instead of the code that checks the length of the slice and indexes into specific locations. This clarifies what Config::build is doing, because the iterator will access the values.

Once Config::build takes ownership of the iterator and stops using indexing operations that borrow, we can move the String values from the iterator into Config rather than calling clone and making a new allocation.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

52

Chapter 13 · 13.3 Improving Our I/O Project

Using the returned iterator directly

Open your I/O project’s src/main.rs file, which should look like this:

src/main.rs

fn main() {

    let args: Vec<String> = env::args().collect();

 

    let config = Config::build(&args).unwrap_or_else(|err| {

        eprintln!("Problem parsing arguments: {err}");

        process::exit(1);

    });

 

    // --snip--

}

First, change the start of the main function from Listing 12.24 to the code in Listing 13.18, which this time uses an iterator. This won’t compile until we update Config::build as well.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

53

Chapter 13 · 13.3 Improving Our I/O Project

Passing env::args to Config::build

Listing 13.18: Passing the return value of env::args to Config::build — src/main.rs

fn main() {

    let config = Config::build(env::args()).unwrap_or_else(|err| {

        eprintln!("Problem parsing arguments: {err}");

        process::exit(1);

    });

 

    // --snip--

}

The env::args function returns an iterator! Rather than collecting the iterator values into a vector and then passing a slice to Config::build, we now pass ownership of the iterator returned from env::args to Config::build directly.

Next, update the definition of Config::build: change its signature to look like Listing 13.19. This still won’t compile, because we need to update the function body.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

54

Chapter 13 · 13.3 Improving Our I/O Project

A signature that expects an iterator

Listing 13.19: Updating the signature of Config::build to expect an iterator — src/main.rs

impl Config {

    fn build(

        mut args: impl Iterator<Item = String>,

    ) -> Result<Config, &'static str> {

        // --snip--

Remark. The standard library documentation for env::args shows that the type of the iterator it returns is std::env::Args, and that type implements the Iterator trait and returns String values.

The parameter args now has a generic type with the trait bounds impl Iterator<Item = String> instead of &[String]. This use of the impl Trait syntax (Chapter 10, “Using Traits as Parameters”) means that args can be any type that implements the Iterator trait and returns String items.

Because we’re taking ownership of args and will mutate it by iterating over it, we add the mut keyword to the args parameter to make it mutable.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

55

Chapter 13 · 13.3 Improving Our I/O Project

Using Iterator trait methods

Next, fix the body of Config::build. Because args implements the Iterator trait, we know we can call the next method on it! Listing 13.20 updates the code from Listing 12.23 to use next.

Listing 13.20: Changing the body of Config::build to use iterator methods — src/main.rs

impl Config {

    fn build(

        mut args: impl Iterator<Item = String>,

    ) -> Result<Config, &'static str> {

        args.next();

 

        let query = match args.next() {

            Some(arg) => arg,

            None => return Err("Didn't get a query string"),

        };

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

56

Chapter 13 · 13.3 Improving Our I/O Project

Moving owned Strings into Config

Listing 13.20 (continued): Changing the body of Config::build to use iterator methods — src/main.rs

 

        let file_path = match args.next() {

            Some(arg) => arg,

            None => return Err("Didn't get a file path"),

        };

 

        let ignore_case = env::var("IGNORE_CASE").is_ok();

 

        Ok(Config {

            query,

            file_path,

            ignore_case,

        })

    }

}

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

57

Chapter 13 · 13.3 Improving Our I/O Project

How the new Config::build reads its arguments

  1. The first value in the return value of env::args is the name of the program. We want to ignore it, so we first call next and do nothing with the return value.
  2. Then we call next to get the value for the query field of Config. If next returns Some, a match extracts the value. If it returns None, not enough arguments were given, and we return early with an Err value.
  3. We do the same thing for the file_path value.

Clarifying code with iterator adapters

We can also take advantage of iterators in the search function of the I/O project, reproduced in Listing 13.21 as it was in Listing 12.19.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

58

Chapter 13 · 13.3 Improving Our I/O Project

The loop-based search function

Listing 13.21: The implementation of the search function from Listing 12.19 — src/lib.rs

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {

    let mut results = Vec::new();

 

    for line in contents.lines() {

        if line.contains(query) {

            results.push(line);

        }

    }

 

    results

}

We can write this code more concisely using iterator adapter methods, which also lets us avoid a mutable intermediate results vector. The functional programming style prefers to minimize the amount of mutable state to make code clearer. Removing the mutable state might enable a future enhancement to make searching happen in parallel, because we wouldn’t have to manage concurrent access to the results vector.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

59

Chapter 13 · 13.3 Improving Our I/O Project

search with filter and collect

Listing 13.22: Using iterator adapter methods in the implementation of the search function — src/lib.rs

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {

    contents

        .lines()

        .filter(|line| line.contains(query))

        .collect()

}

The purpose of search is to return all lines in contents that contain the query. Similar to the filter example in Listing 13.16, this code uses the filter adapter to keep only the lines for which line.contains(query) returns true. We then collect the matching lines into another vector with collect. Much simpler!

Feel free to make the same change to use iterator methods in the search_case_insensitive function as well.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

60

Chapter 13 · 13.3 Improving Our I/O Project

Going further: return an iterator from search

For a further improvement, return an iterator from the search function, so that the function becomes an iterator adapter:

  • remove the call to collect;
  • change the return type to impl Iterator<Item = &'a str>;
  • note that you’ll also need to update the tests!

Search through a large file using your minigrep tool before and after making this change to observe the difference in behavior. Before the change, the program won’t print any results until it has collected all of the results. After the change, the results are printed as each matching line is found, because the for loop in the run function is able to take advantage of the laziness of the iterator.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

61

Chapter 13 · 13.3 Improving Our I/O Project

Choosing between loops and iterators

Which style should you choose in your own code, and why: the original implementation in Listing 13.21 or the iterator version in Listing 13.22 (assuming we collect all the results before returning them rather than returning the iterator)?

  • Most Rust programmers prefer the iterator style. It’s a bit tougher to get the hang of at first, but once you get a feel for the various iterator adapters and what they do, iterators can be easier to understand.
  • Instead of fiddling with the various bits of looping and building new vectors, the code focuses on the high-level objective of the loop.
  • This abstracts away some of the commonplace code, so it’s easier to see the concepts that are unique to this code, such as the filtering condition each element in the iterator must pass.

But are the two implementations truly equivalent? The intuitive assumption might be that the lower-level loop will be faster. Let’s talk about performance.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

62

Chapter 13 · 13.4 Performance in Loops vs. Iterators

Benchmarking the two search versions

To decide between loops and iterators, you need to know which implementation is faster: the version of search with an explicit for loop or the version with iterators.

A benchmark loaded the entire contents of The Adventures of Sherlock Holmes by Sir Arthur Conan Doyle into a String and looked for the word “the” in the contents. Here are the results for the version of search using the for loop and the version using iterators:

test bench_search_for  ... bench:  19,620,300 ns/iter (+/- 915,700)

test bench_search_iter ... bench:  19,234,900 ns/iter (+/- 657,200)

The two implementations have similar performance! The benchmark code isn’t explained here, because the point is not to prove that the two versions are equivalent but to get a general sense of how they compare performance-wise.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

63

Chapter 13 · 13.4 Performance in Loops vs. Iterators

Iterators are a zero-cost abstraction

For a more comprehensive benchmark, check using various texts of various sizes as the contents, different words and words of different lengths as the query, and all kinds of other variations. The point is this: iterators, although a high-level abstraction, get compiled down to roughly the same code as if you’d written the lower-level code yourself.

Definition 13.11 (Zero-cost abstraction). An abstraction whose use imposes no additional runtime overhead. Iterators are one of Rust’s zero-cost abstractions.

This is analogous to how Bjarne Stroustrup, the original designer and implementor of C++, defines zero-overhead in his 2012 ETAPS keynote presentation “Foundations of C++”:

“In general, C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.”

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

64

Chapter 13 · 13.4 Performance in Loops vs. Iterators

Use iterators and closures without fear

  • In many cases, Rust code using iterators compiles to the same assembly you’d write by hand.
  • Optimizations such as loop unrolling and eliminating bounds checking on array access apply, and make the resultant code extremely efficient.

Now that you know this, you can use iterators and closures without fear! They make code seem like it’s higher level but don’t impose a runtime performance penalty for doing so.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

65

Chapter 13 · Summary

Chapter summary

Closures and iterators are Rust features inspired by functional programming language ideas. They contribute to Rust’s capability to clearly express high-level ideas at low-level performance. The implementations of closures and iterators are such that runtime performance is not affected. This is part of Rust’s goal to strive to provide zero-cost abstractions.

  • Closures capture their environment by immutable borrow, mutable borrow, or move (forced with move), and implement FnOnce, FnMut and Fn according to what their body does.
  • Iterators are lazy: adapters such as map and filter do nothing until a consuming adapter such as sum or collect uses them up.
  • In minigrep, taking an iterator removed the clone calls, and filter replaced the mutable results vector.

Remark. Now that the expressiveness of the I/O project has improved, the next chapter looks at some more features of cargo that will help us share the project with the world.

The Rust Programming Language · Ch.13 Functional Language Features: Iterators and Closures

66