0% found this document useful (0 votes)
3 views29 pages

03 Code Flow

This chapter discusses pattern matching and its role in controlling code flow and handling errors in Rust, emphasizing its importance in functional programming. It explains how pattern matching can unwrap values and destructure data types, enhancing code readability and safety by ensuring all cases are handled. Additionally, the chapter highlights the use of the '?' operator for cleaner error handling in functions that return Result or Option types.

Uploaded by

Game Pvp
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views29 pages

03 Code Flow

This chapter discusses pattern matching and its role in controlling code flow and handling errors in Rust, emphasizing its importance in functional programming. It explains how pattern matching can unwrap values and destructure data types, enhancing code readability and safety by ensuring all cases are handled. Additionally, the chapter highlights the use of the '?' operator for cleaner error handling in functions that return Result or Option types.

Uploaded by

Game Pvp
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Code flow

This chapter covers


 Discussing pattern matching
 Handling errors with pattern matching
 Reviewing Rust’s functional programming
patterns

We need to continue to review more of Rust’s core language features—its building


blocks—before diving into design patterns. In this chapter, we’ll start by discussing
pattern matching and functional programming. Pattern matching allows us to con-
trol the code flow, unwrap or destructure values, and handle optional cases. Func-
tional programming lets us build software around the unit of a function, which is one
of the most basic and easiest-to-understand abstractions.
These building blocks are distinct but can be combined in many ways to create
new abstractions. We’ll tie these building blocks together to create more elaborate
design patterns by combining them in various ways. In cooking (to use an analogy),
we employ four essential elements in different combinations from multiple sources
to create delicious foods: salt, fat, acid, and heat. Before making patterns based on
these elements, we must understand them in depth.

34
3.1 A tour of pattern matching 35

3.1 A tour of pattern matching


Up to now, we’ve discussed generics and traits that make up Rust’s core compile-time
features. Pattern matching is a run-time feature that enables a variety of lovely code flow
patterns. We can match types, values, enum variants, and more. Rust’s pattern match-
ing is powerful because it supports several kinds of matching (on both values and
types); most important, it enables clean, functional programming patterns.

NOTE Pattern matching is not to be confused with design patterns. Pattern match-
ing is a core language feature of Rust (and other languages), and although we
can use it to build design patterns, it isn’t strictly a design pattern.

If you’ve used a switch/case statement, Rust’s pattern matching will look familiar. But
Rust’s pattern matching is much more potent than a switch/case statement. Some lan-
guages provide an equivalent feature, but pattern matching is still somewhat niche,
and many mainstream languages do not have it. Pattern matching likely saw its first
widespread use in Prolog and is an essential feature of functional languages such as
Haskell, Scala, Erlang (itself influenced by and initially implemented in Prolog),
Elixir, and OCaml.
A basic pattern match starts with the match keyword, which makes it easy to rec-
ognize. As with a switch/case statement, we list all the patterns we want to match
with an optional catch-all at the end. In Rust, however, we have to match all possible
patterns or provide the catch-all case. The Rust compiler tells us if we’re missing a
case with an error.

3.1.1 Basics of pattern matching


A simple example of pattern matching is unwrapping an Option and printing whether
it contains a value.

Listing 3.1 Pattern matching an Option

fn some_or_none<T>(option: &Option<T>) {
match option {
We unwrap the option’s value into
_v. Prefixing a variable with an
Some(_v) => println!("is some!"),
underscore tells the compiler
None => println!("is none :("),
that the value is unneeded.
}
}

Unwrapping Option, Result, or other structures that contain optional data is a com-
mon use of pattern matching. Using pattern matching to unwrap data is arguably the
killer feature of pattern matching because the compiler requires us to handle all
cases. It takes the guesswork out of knowing whether you’ve handled all possible cases.
Pattern matching cannot guarantee that your code is free of logic errors; instead, it
makes code easier to reason about.
An astute reader may notice that in listing 3.1, we discarded the value of Some(_v),
but it would be nice to print its value instead. To do so, we need to use a binding in
36 CHAPTER 3 Code flow

our pattern match and update the generic parameter T to include a trait bound for
std::fmt::Display.

Listing 3.2 Pattern matching an Option with a display trait bound

fn some_or_none_display<T: std::fmt::Display>(option: &Option<T>) {


match option {
Some(v) => println!("is some! where v={v}"),
None => println!("is none :("),
}
}

Now we can call some_or_none_display() with an Option that contains any value that
implements std::fmt::Display and print the value if it’s Some.

Sourcing security vulnerabilities


The vast majority of critical security vulnerabilities in software tend to involve the
same class of problems: memory safety. An analysis by Microsoft ([Link]
yZKy) found that 70% of security vulnerabilities in Microsoft products involved mem-
ory safety bugs in C and C++ code. Examples of memory safety problems include
 Reading/writing outside the bounds of an array
 Dereferencing invalid pointers, such as null pointers
 Using memory after it’s been freed
 Attempting to free memory that was previously freed (such as double-free)
 Failing to handle error cases
Rust’s safety features seek to eliminate these cases, and pattern matching is a key
feature that helps programmers avoid common pitfalls by requiring that all cases be
handled. Pattern matching on an Option into Some and None is a good example of
how Rust forces us to handle all possible cases.
Choosing Rust for critical software is akin to buying an insurance policy or a put con-
tract (a financial instrument that protects against catastrophic loss). Rust is a way to
hedge against the risk of security vulnerabilities and protect your users and your rep-
utation. The premiums you pay are the time and effort involved in learning Rust’s
safety features, the discipline required to use them, and any additional cognitive load
on your part. The payout is peace of mind from knowing that your software is less
likely to be the next headline in a security breach. The simple tradeoff is a little extra
work upfront for a lot less work later should things go wrong.

Pattern matching isn’t limited to unwrapping Option types, although that use case is
common. We can also match specific integral values, including ranges:

fn what_type_of_integer_is_this(value: i32) {
match value {
1 => println!("The number one number"),
2 | 3 => println!("This is a two or a three"),
3.1 A tour of pattern matching 37

4..=10 => println!("This is a number between 4 and 10 (inclusive)"),


_ => println!("Some other kind of number"),
}
}

Pattern matching is often used to destructure structs, tuples, and enums. You can
destructure tuples partially or pull out each element, which can be a convenient way
to access inner elements in some cases:

fn destructure_tuple(tuple: &(i32, i32, i32)) {


match tuple { Matches only on the
(first, ..) => { first element in a
println!("First tuple element is {first}") tuple of any length
}
}
match tuple { Matches only on the
(.., last) => { last element in a tuple
println!("Last tuple element is {last}") of any length
}
}
match tuple {
(_, middle, _) => { Matches the middle
println!( element on a tuple
"The middle tuple element is {middle}" with three elements
)
}
Matches every
} element of a tuple
match tuple { with three elements
(first, middle, last) => {
println!("The whole tuple is ({first}, {middle}, {last})")
}
}
}

You can have multiple equivalent match expressions, but the block always returns the
expression from the first matching pattern. In the preceding example, we use a sepa-
rate match block for each case because all matches are valid. If you have multiple
equivalent patterns in a match block, your code will compile but produce a warning,
like the following code snippet:

fn unreachable_pattern_match(value: i32) {
match value {
1 => println!("This value is equal to 1"),
1 => println!("This value is equal to 1"),
_ => println!("This value is not equal to 1"),
}
}

Compiling this code will produce the following warning for the second match case:

warning: unreachable pattern


--> src/[Link]:9
38 CHAPTER 3 Code flow

|
56 | 1 => println!("Second match: This value is equal to 1"),
| ^
|
= note: `#[warn(unreachable_patterns)]` on by default

A guard allows you to match conditionally by using an if statement after the pattern,
which can use the matched value or a separate value passed to the guard. The follow-
ing code uses a guard to match on a value and a Boolean:

fn match_with_guard(value: i32, choose_first: bool) {


match value {
v if v == 1 && choose_first => {
println!("First match: This value is equal to 1")
}
v if v == 1 && !choose_first => {
println!("Second match: This value is equal to 1")
}
v if choose_first => {
println!("First match: This value is equal to {v}")
}
v if !choose_first => {
println!("Second match: This value is equal to {v}")
}
_ => println!("Fell through to the default case"),
}
}

You can’t match values of different types within a match statement. All match cases or
branches within the same match {} block should apply to the same type. The match
block is an expression, so each branch (and each expression therein) needs to return
the same type. You can unwrap structures that contain different types (such as an
enum), but you can’t match generically. The following code, for example, is not valid:

fn invalid_matching<T>(value: &T) {
match value {
"is a string" => println!("This is a string"),
1 => println!("This is an integral value"),
}
}

Attempting to compile this code will produce the following compiler output:

error[E0308]: mismatched types


--> src/[Link]:9
|
1 | fn invalid_matching<T>(value: &T) {
| - this type parameter
2 | match value {
| ----- this expression has type `&T`
3 | "is a string" => println!("This is a string"),
| ^^^^^^^^^^^^^ expected `&T`, found `&str`
3.1 A tour of pattern matching 39

|
= note: expected reference `&T`
found reference `&'static str`

error[E0308]: mismatched types


--> src/[Link]:9
|
1 | fn invalid_matching<T>(value: &T) {
| - this type parameter
2 | match value {
| ----- this expression has type `&T`
3 | "is a string" => println!("This is a string"),
4 | 1 => println!("This is an integral value"),
| ^ expected type parameter `T`, found integer
|
= note: expected type parameter `T`
found type `{integer}`

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

We can destructure different inner types if we use an enum. DistinctTypes allows us


to match distinct named types in match_enum_types(), just as you would an Option:

enum DistinctTypes {
Name(String),
Count(i32),
}

fn match_enum_types(enum_types: &DistinctTypes) {
match enum_types {
DistinctTypes::Name(name) => println!("name={name}"),
DistinctTypes::Count(count) => println!("count={count}"),
}
}

We can destructure structs to extract specific values and even match on particular val-
ues within a struct, as I’ll demonstrate in the following example. This code snippet
creates an enum for cat colors, a struct that contains the cat’s name and its color, and
a function match_on_black_cats() that prints the cat’s name and tells us whether it’s
a black cat:

enum CatColor {
Black,
Red,
Chocolate,
Cinnamon,
Blue,
Cream,
Cheshire,
}

struct Cat {
name: String,
40 CHAPTER 3 Code flow

color: CatColor,
}

fn match_on_black_cats(cat: &Cat) {
match cat {
Cat {
name,
color: CatColor::Black,
} => println!("This is a black cat named {name}"),
Cat { name, color: _ } => println!("{name} is not a black cat"),
}
}

We can quickly test the code as follows:

let black_cat = Cat {


name: String::from("Henry"),
color: CatColor::Black,
};
let cheshire_cat = Cat {
name: String::from("Penelope"),
color: CatColor::Cheshire,
};
match_on_black_cats(&black_cat);
match_on_black_cats(&cheshire_cat);

Running the preceding test prints the following output:

This is a black cat named Henry


Penelope is not a black cat

3.1.2 Clean matches with the ? operator


Pattern matching is an excellent way to handle errors, but code can get messy when
we have too many matches or matches that are too deeply nested. We can combine
pattern matching with the ? operator to handle functions that return Result or
Option cleanly by returning immediately when Result or Option returns an error or
None, respectively. To use the ? operator, we need to be inside a function that returns
Result or Option. The ? operator allows us to flatten our code considerably, which
improves readability:

Our function returns a std::io::Result, which is a type alias


for Result with the std::io::Error error type provided for
convenience. The return payload is a unit ().
fn write_to_file() -> std::io::Result<()> {
use std::fs::File; All calls to functions returning a
use std::io::prelude::*; Result use the ? operator to denote
that in case of an error, the function
let mut file = File::create("filename")?; should return that error.
file.write_all(b"File contents")?;
Ok(()) We return the unit type
}
with Ok to show success.
3.1 A tour of pattern matching 41

fn try_to_write_to_file() {
Calls our function and
match write_to_file() { matches on the result
Ok(()) => println!("Write succeeded"),
Err(err) => println!("Write failed: {}", err.to_string()),
}
}

In the preceding code, we wrap the call to write_to_file() within a pattern-matching


expression. If the function returns Ok(()), we print Write succeeded. In the case of
an error, we print Write failed: … with the error message.
Using the ? operator is a super-handy way to keep your code clean by using Result.
Notice that I used the unit type (), a special type in Rust that is essentially a place-
holder that carries no value and is optimized out by the compiler. The unit type () is
often referred to simply as unit. The equivalent code without ? looks something like
this example, which includes duplicate code for printing the error case:

fn write_to_file_without_result() {
use std::fs::File;
use std::io::prelude::*;

let create_result = File::create("filename");


match create_result {
Ok(mut file) => match file.write_all(b"File contents") {
Err(err) => {
println!("There was an error writing: {}", err)
}
_ => println!("Write succeeded"),
},
Err(err) => println!(
"There was an error opening the file: {}",
err
),
}
}

If we want to chain lots of calls by using the ? operator, we need to pay attention to
their return types. The ? operator works only with functions that return either a
Result<T, E> or Option<T> that matches the type of the statement with the ? applied.
For Result<T, E>, the error types of all the functions using ? must match the parent
function or provide an implementation of the From trait so that they can be converted
to the target error type. For this reason, you’ll often have to write impl From for … {}
for conversion between error types.

TIP When you’re chaining the ? operator, you can use a few handy methods
for converting between Result and Option, in addition to implementing the
From trait. For Result<T, E>, you can use the ok() method to map to
Option<T>, err() to map to Option<E>, and map_err() to map an error to a
different type. For Option<T>, use ok_or() to map to Result<T,E>.
42 CHAPTER 3 Code flow

In the preceding example, if we want to use our own error type instead of
std::io::Error, perhaps because we want to add more information to the original
error, we need to do something like this:

enum ErrorTypes {
IoError(std::io::Error),
FormatError(std::fmt::Error),
}

struct ErrorWrapper {
source: ErrorTypes,
message: String,
}

Next, we need to implement From<std::io::Error> for our error wrapper:

impl From<std::io::Error> for ErrorWrapper {


fn from(source: std::io::Error) -> Self {
Self {
source: ErrorTypes::IoError(source),
message: "there was an IO error!".into(),
}
}
}

Now we can update our file-writing code to use our error type by returning Error-
Wrapper in our write_to_file() function:

fn write_to_file() -> Result<(), ErrorWrapper> {


Returns a plain Result
use std::fs::File; instead of std::io::Result
use std::io::prelude::*; using our error type
let mut file = File::create("filename")?;
file.write_all(b"File contents")?;
Ok(())
}

fn try_to_write_to_file() {
match write_to_file() { Prints our error
Ok(()) => println!("Write succeeded"), message instead of
Err(err) => { the one provided
println!("Write failed: {}", [Link]) by std::io::Error
}
}
}

If we call our try_to_write_to_file() function, it should (under normal circum-


stances) print Write succeeded. But in the case of an error (such as not having per-
mission to write a file), the function will print Write failed: … with the error message
provided by File.
Handling errors this way is fairly common in Rust and can save a great deal of
typing. This approach is a relatively simple way to integrate errors from third-party
3.2 Functional Rust 43

crates into your error-handling code. Chapter 4 revisits the ? operator and error han-
dling in Rust.

3.2 Functional Rust


So far, this book has covered the basics: generics, traits, and pattern matching. Now
we’ll move on to Rust’s functional features, including one of my favorite subjects: func-
tional programming. The two core features of functional programming in Rust are clo-
sures and iterators.
Many people have probably used closures and iterators at some point, as they’ve
become trendy. The JavaScript and TypeScript languages and their libraries, for
example, make heavy use of closures. Iterators are so common that most people
don’t think of them as abstractions but as a core feature of all modern program-
ming languages.
Functional programming is a paradigm wherein programs are composed of declar-
ative functions, and mutation of state is discouraged (though not necessarily disal-
lowed, depending on the strictness of the language). Some languages are strictly
functional, which means that you’re not allowed to change state; the only way to affect
state is to use a function that maps one value to another. Also, functional languages
discourage side effects, which are actions within a function that might have nondeter-
ministic results, such as I/O or mutating local state.
To support functional programming, some languages have features explicitly
designed around functions and handling immutable state. Although Rust is not
strictly functional, it encourages functional patterns by making mutability opt-in (with
the mut keyword) rather than opt-out and by providing core functional features such
as closures and iterators.
Functional programming is a wide subject, so I’ll stick to reviewing the high-level
features in Rust. For a deep dive into functional programming, Grokking Functional Pro-
gramming by Michał Płachta ([Link]
-programming) provides an excellent overview.

3.2.1 Basics of functional programming in Rust


Let’s jump in by looking at a simple (but not pure) closure:

let bark = || println!("Bark!");


Calling println!() introduces side effects because it’s
bark(); an I/O operation, meaning this closure is not pure.

Here, we have a function that barks like a dog with "Bark!" It doesn’t look like a func-
tion because it has no arguments, and the braces have been removed, as they’re not nec-
essary. In Rust, closures begin with a list of arguments between two pipes, ||, followed by
a code block. In the case of a single-line function, you can omit the braces ({}) for the
block. Let’s add a parameter to make the function look more function-like:

let increment = |value| value + 1;


increment(1);
44 CHAPTER 3 Code flow

Here, the function takes an integer value and returns that value plus 1. We don’t
need to specify the type of the value parameter because the compiler can infer it.
Let’s make a closure that looks even more function-like by using a code block:

let print_and_increment = |value| {


println!("{value} will be incremented and returned");
value + 1
};
print_and_increment(5);

These examples aren’t too interesting. Closures start to get interesting when we talk
about higher-order functions, which take other functions as parameters. In Rust, you may
have encountered higher-order functions when working with iterators, specifically
when using map(), for_each(), find(), fold(), and similar methods. Higher-order
functions are a convenient way to delegate operations to the caller of the function by
allowing the caller to supply inner logic to the callee. Closures make the syntax more
convenient, delightful, and flexible. The following simple example of using a higher-
order function creates an adder that gets its values from other functions:

let left_value = || 1;
A closure that returns 1 and
let right_value = || 2; provides impl Fn() -> i32
let adder = |left: fn() -> i32,
right: fn() -> i32| {
A closure that returns 2 and
left() + right()
provides impl Fn() -> i32
};
println!(
"{} + {} = {}", A closure that takes two functions
left_value(), and adds their results together,
right_value(), providing impl Fn(fn() -> i32,
adder(left_value, right_value) fn() -> i32) -> i32
);

The preceding example has two closures, assigned to left_value and right_value,
respectively, that return a hardcoded integer. Then we create this adder, which takes
two parameters of type fn() → i32, a special function type. We can pass any function
that matches the signature to the adder. In this case, we add the left and right values
together, which is 1 + 2, so our function returns 3. Running this code produces the fol-
lowing output:

1 + 2 = 3

You can experiment by changing the values returned by left_value and right_
value; you’ll see the output change accordingly. You can also try changing the adder
to multiply the values instead of adding them.
3.2 Functional Rust 45

3.2.2 Closure variable capture


If we want to call our adder with a function that doesn’t have the proper signature, we
could wrap it with another closure to get the correct signature. Let’s discuss variable
capture in closures to understand why we might need to do this.
Rust provides three traits that aid in functional programming: Fn, FnMut, and
FnOnce. These traits are implemented automatically when possible and summarized
as follows:
 Fn is for functions in the form of Fn(&self), which can be called repeatedly, as
they don’t consume the variables they capture. All arguments are immutable.
 FnMut is for mutable functions, such as those of the form FnMut(&mut self).
They can be called repeatedly, as they don’t consume the variables they capture,
but they do contain mutable references.
 FnOnce is for functions that consume themselves, such as FnOnce(self). They
can be called only once because they consume the variables they capture.
In the case of closures, FnOnce is always implemented if the closure consumes any of
the variables it captures, denoted by the move keyword before the definition of a clo-
sure. Consider the closure in the following listing.

Listing 3.3 Closure with move

let consumable = String::from("cookie");


let consumer = move || consumable;
consumer();
// consumer(); error!

In this example, the fourth line would produce an error because our consumable
can be moved only once, so calling consumer() a second time is invalid. If we try
compiling with the second call to consumer() uncommented, we’ll get the follow-
ing output from the compiler:

error[E0382]: use of moved value: `consumer`


--> src/[Link]:5
|
21 | consumer();
| ---------- `consumer` moved due to this call
22 | consumer();
| ^^^^^^^^ value used here after move
|
note: closure cannot be invoked more than once because it moves the
➥ variable `consumable` out of its environment
--> src/[Link]:28
|
20 | let consumer = move || consumable;
| ^^^^^^^^^^
note: this value implements `FnOnce`, which causes it to be moved when
➥ called
--> src/[Link]:5
46 CHAPTER 3 Code flow

|
21 | consumer();
| ^^^^^^^^

For more information about this error, try `rustc --explain E0382`.
error: could not compile `closures` (bin "closures") due to 1 previous
➥ error

The primary use of move |…| (as in listing 3.3) is when you want to transfer or assign
ownership of an object somewhere inside the closure but avoid copying or cloning it.
The move keyword is optional; if you don’t use it, Rust infers whether to move the vari-
ables you capture. Still, being explicit about your intentions is a good idea because it
prevents ambiguity. The compiler will alert you if an error occurs, of course. In the
example with consumable, we could have omitted the move keyword safely; the result
would have been the same. We can combine the use of closures, generics, and the Fn,
FnMut, and FnOnce traits to enable a variety of generic functional patterns.

3.2.3 Examining iterators


Let’s take a look at Rust’s iterators, which complement closures. Rust’s iterators are pro-
vided by the Iterator trait, which includes a lot of functionality built on top of iterators:
map(), for_each(), take(), fold(), filter() find(), zip(), and more. If you imple-
ment the Iterator trait for your type, you receive all these iterators (and more!).
Iterators are one of the original Gang of Four design patterns and arguably the
most prolific. They provide a great case study not only for design patterns but also for
the Rust language. The core of Rust’s Iterator trait is as follows:

trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}

The Iterator trait contains a lot more than what you see here, but if you want to
implement Iterator for your type, you need to provide only next() and Item. Let’s
examine an example of a linked list in Rust by implementing the Iterator trait. We’ll
start by writing a new linked list implementation.

Listing 3.4 Implementing LinkedList

use std::cell::RefCell;
use std::rc::Rc;

type ItemData<T> = Rc<RefCell<T>>;


type ListItemPtr<T> = Rc<RefCell<ListItem<T>>>;

struct ListItem<T> { A pointer to


data: ItemData<T>, our data
next: Option<ListItemPtr<T>>,
A pointer to the next
} item in the linked list
3.2 Functional Rust 47

impl<T> ListItem<T> {
Creates a new item
fn new(t: T) -> Self {
(or node) for the list
Self {
data: Rc::new(RefCell::new(t)),
next: None,
}
}
}

struct LinkedList<T> { A pointer to the first item


head: ListItemPtr<T>, (or node) in the list
}

impl<T> LinkedList<T> { Creates a new list, with the


fn new(t: T) -> Self { head pointing to the first item
Self {
head: Rc::new(RefCell::new(ListItem::new(t))),
}
}
}

We have an incomplete linked list that has the structure we need but doesn’t give us a
way to iterate over the list or append new items. I intentionally left out the append
functionality because I want to use an iterator to implement it. If I implement Iterator
first, the rest of the linked list features become easy to add. Let’s give it a shot.

Rc and RefCell
If you haven’t encountered Rc or RefCell (introduced in listing 3.4), don’t panic; I’ll
provide a brief explanation for readers who aren’t familiar with them. In short, Rc and
RefCell are smart pointers that provide important (but distinct) features.

Rc provides a reference-counted pointer, similar to C++’s std::shared_ptr. RefCell


is a special type of pointer that enables interior mutability.

Rc allows you to hold multiple references (or pointers) to the same location in mem-
ory, and RefCell provides a way to perform borrow checking at run time. Rust’s bor-
row checker normally works at compile time, but sometimes you want to perform the
borrow checking at run time instead, such as when you want to hold multiple refer-
ences to the same object and still enable mutability (not possible at compile time).

In our linked list example, we need to hold multiple references to the same object
(which Rc provides), and we also want to be able to mutate the inner object (which
RefCell allows us to do safely).

In chapter 5 of Code Like a Pro in Rust ([Link]


-a-pro-in-rust), I discuss Rust’s smart pointers at great length. For details on Rc, con-
sult the Rust standard library documentation at [Link]
[Link], and for RefCell, refer to [Link]
48 CHAPTER 3 Code flow

I’ll note here that iterators are stateful. That is, an iterator knows where it is in the
sequence of items so that it can go from the previous to the next item with each
subsequent call to next().

NOTE Even in the purest functional programming languages, you can always
find state under the hood if you look hard enough, as all software eventually
breaks down to strictly imperative machine code.

For now, we’ll store that state in our linked list itself. We can update the structure this
way, along with the fn new() method:

struct LinkedList<T> {
head: ListItemPtr<T>,
cur_iter: Option<ListItemPtr<T>>,
}

impl<T> LinkedList<T> {
fn new(t: T) -> Self {
Self {
head: Rc::new(RefCell::new(ListItem::new(t))),
cur_iter: None,
}
}
}

Great! Now we have a pointer to the current position of our iterator in cur_iter,
which can be initialized to None. Let’s take a first shot at implementing the Iterator
trait for our linked list (not the refined approach, which we’ll arrive at later in this
chapter):
For this Iterator implementation,
we’ll return a pointer to the list
item rather than the data itself.
impl<T> Iterator for LinkedList<T> { We have to clone cur_iter
type Item = ListItemPtr<T>; here because we try to
fn next(&mut self) -> Option<Self::Item> { modify the pointer while
match &self.cur_iter.clone() { it’s borrowed later.
None => {
self.cur_iter = Some([Link]());
If cur_iter is
} None, the iterator is
Some(ptr) => { uninitialized, so we
self.cur_iter = [Link]().[Link](); start at the head.
}
} cur_iter must be updated
self.cur_iter.clone()
to point to the next item
in the sequence.
}
} Last, we clone and return the
current position in our sequence.

Now finding the last item in the list with an iterator is a trivial operation:

let dinosaurs = LinkedList::new("Tyrannosaurus Rex");


let last_item = [Link]()
3.2 Functional Rust 49

.expect("couldn't get the last item");


println!("last_item='{}'", last_item.borrow().[Link]());

By implementing Iterator, we can call last() to retrieve the last item in our list, which
we get for free from the Iterator trait. Running the preceding code prints last_item=
'Tyrannosaurus Rex', as we’d expect. Now let’s add our append() method to the origi-
nal LinkedList:

impl<T> LinkedList<T> {
fn new(t: T) -> Self {
Self {
head: Rc::new(RefCell::new(ListItem::new(t))),
cur_iter: None,
}
}
fn append(&mut self, t: T) { We must borrow
[Link]() the inner RefCell
.expect("List was empty, but it should never be") to access the inner
.as_ref() ListItem.
.borrow_mut()
.next = Some(Rc::new(RefCell::new(ListItem::new(t))));
}
We have to borrow mutably to
}
modify the inner next pointer.

Now we can append and then iterate over our list by using for_each with a closure:

let mut dinosaurs = LinkedList::new("Tyrannosaurus Rex");


[Link]("Triceratops");
[Link]("Velociraptor");
[Link]("Stegosaurus");
[Link]("Spinosaurus"); We still have to
dinosaurs unwrap the inner
.iter() pointer here, and our
.for_each(|ptr| { call to for_each() will
println!("data={}", [Link]().[Link]()) consume dinosaurs.
);

Running this code prints the following:

data=Tyrannosaurus Rex
data=Triceratops
data=Velociraptor
data=Stegosaurus
data=Spinosaurus

NOTE The code in this example doesn’t match the final implementation and,
therefore, doesn’t match the code in the repository, but we’ll get there soon.

Neat, huh? This example is fun, but our iterator is less than ideal because we still have
to unwrap the internal pointer to access our payload data within each node of the
50 CHAPTER 3 Code flow

linked list. In my opinion, this interface is pretty awkward for a collection type. We
probably wouldn’t want to expose our internal types if we were writing a library.

3.2.4 Obtaining an iterator with iter(), into_iter(), and iter_mut()


To make our linked list more idiomatic, we need to iterate over items in the list with-
out exposing the internal structure of the list. We also need to iterate over mutable
references to the items in the list and to consume the list and iterate over the items. In
other words, we may want to iterate over our linked list in three ways:

 iter()—Iterate over immutable references to the items in the list.


 iter_mut()—Iterate over mutable references to the items in the list.
 into_iter()—Consume the list and iterate over the items.

In section 3.2.3, I implemented the Iterator trait directly on LinkedList, but this is
not idiomatic Rust, and it’s bad practice. Instead, we’ll create a separate structure to
handle iteration, which is a common pattern in Rust and better design. If we look at
Rust’s built-in collection types, they typically provide three iterators:

 An iterator that iterates over T, provided by into_iter(self), which consumes


self
 An iterator that iterates over &T, provided by iter(&self)
 An iterator that iterates over &mut T, provided by iter_mut(&mut self)

You’ll notice that Vec does not implement the Iterator trait directly; instead, it
implements the IntoIterator trait for T, &T, and &mut T. Vec uses its own internal
([Link] Iter, IterMut, and IntoIter
objects to implement the Iterator trait instead of doing it directly on Vec. We can
do the same with our linked list by creating separate structures to handle iteration
rather than implementing Iterator for LinkedList.
Let’s copy this pattern and apply it to our linked list. First, we’ll create our new
stateful iterator structs, which look like this:

struct Iter<T> {
next: Option<ListItemPtr<T>>,
}
struct IterMut<T> {
next: Option<ListItemPtr<T>>,
}
struct IntoIter<T> {
next: Option<ListItemPtr<T>>,
}

Each iterator struct maintains a pointer to the next item in the list. Because we’re
using Rc and RefCell to implement the linked list, managing the pointers is fairly
easy, and we don’t have to worry much about lifetimes.
3.2 Functional Rust 51

We’ll initialize these iterators by adding iter(), iter_mut(), and into_iter()


methods to LinkedList, which returns a new instance. We’ll also update our append()
so that it works again:

impl<T> LinkedList<T> {
fn new(t: T) -> Self { We have to unwrap the
Self { inner Option within the
head: Rc::new(RefCell::new(ListItem::new(t))), RefCell and Rc, which is
} why we need to obtain a
} reference with as_ref()
fn append(&mut self, t: T) { and borrow with
let mut next = [Link](); borrow() to access the
while next.as_ref().borrow().next.is_some() { inner next pointer.
let n = next
.as_ref() We have to borrow three
.borrow() times: twice from the
.next current next and once from
.as_ref() the next next, after which
.unwrap() we can clone the pointer.
.clone();
next = n;
}
next.as_ref().borrow_mut().next =
Some(Rc::new(RefCell::new(ListItem::new(t))));
}
fn iter(&self) -> Iter<T> {
Iter {
next: Some([Link]()),
}
}
fn iter_mut(&mut self) -> IterMut<T> {
IterMut {
next: Some([Link]()),
}
}
fn into_iter(self) -> IntoIter<T> {
IntoIter {
next: Some([Link]()),
}
}
}

Cool! We’ve updated append() so that it no longer uses the old Iterator implementa-
tion, which we’ve already decided is flawed. Now all we have to do is implement the
Iterator trait for Iter, IterMut, and IntoIter:

impl<T> Iterator for Iter<T> {


type Item = ItemData<T>;
fn next(&mut self) -> Option<Self::Item> {
match [Link]() {
Some(ptr) => {
[Link].clone_from(&ptr.as_ref().borrow().next);
Some(ptr.as_ref().borrow().[Link]())
52 CHAPTER 3 Code flow

}
None => None,
}
}
}
impl<T> Iterator for IterMut<T> {
type Item = ItemData<T>;
fn next(&mut self) -> Option<Self::Item> {
match [Link]() {
Some(ptr) => {
[Link].clone_from(&ptr.as_ref().borrow().next);
Some(ptr.as_ref().borrow().[Link]())
}
None => None,
}
}
}
impl<T> Iterator for IntoIter<T> {
type Item = ItemData<T>;
fn next(&mut self) -> Option<Self::Item> {
match [Link]() {
Some(ptr) => {
[Link].clone_from(&ptr.as_ref().borrow().next);
Some(ptr.as_ref().borrow().[Link]())
}
None => None,
}
}
}

Our next() implementation is straightforward: we return the pointer to the data


within our ListItem struct, update [Link] to the next item in the list, and return
None when there are no more entries. You may notice that all three implementations
are identical. The situation is even worse: all of them return Rc<RefCell<T>> rather
than the T, &T, and &mut T we’re looking for. Returning Rc<RefCell<T>> is fine, but it
doesn’t match the pattern, and we still have to unwrap the data to access it.
The solution to this problem isn’t straightforward, but let’s try to fix it by looking
at IntoIter from Vec. The into_iter() method on Vec has the following signature:

fn into_iter(self) -> slice::IterMut<'a, T>;

If you look carefully, you’ll see that the method takes self by value. In other words,
calling into_iter() consumes the Vec. We can use this knowledge to change our
IntoIter so that it consumes each list item:

impl<T> Iterator for IntoIter<T> {


type Item = T;
fn next(&mut self) -> Option<Self::Item> {
match [Link]() {
Some(ptr) => {
[Link] = ptr.as_ref().borrow().[Link]();
3.2 Functional Rust 53

let listitem =
Rc::try_unwrap(ptr).map(|refcell| refcell.into_inner());
match listitem {
Ok(listitem) => Rc::try_unwrap([Link])
.map(|refcell| refcell.into_inner())
.ok(),
Err(_) => None,
}
}
None => None,
}
}
}

The code is starting to look a lot more complicated. Let’s break it down:
 Both our pointers to each list item (or node) in the linked list, as well as the
data, are stored in a RefCell inside Rc (i.e., Rc<RefCell<T>>).
 We need to use try_unwrap() on the Rc to move the inner RefCell out of the
Rc because we want to consume it. try_unwrap() works on Rc only when there
are no other references. Because we’re not going to expose these references
outside our linked list, we can be reasonably sure that there aren’t any other
references.
 When we get the RefCell out of the Rc using try_unwrap(), we need to move
the T out of RefCell<T>. To do so, we call into_inner(), which consumes the
RefCell that returns an owned T.
 The return type is defined by type Item = T, which is an associated type, and we
reference it with Self::Item, which is required by the Iterator trait.
We can test our code this way:

let mut dinosaurs = LinkedList::new("Tyrannosaurus Rex");


[Link]("Triceratops");
[Link]("Velociraptor");
[Link]("Stegosaurus");
[Link]("Spinosaurus");
dinosaurs
.into_iter()
.for_each(|data| println!("data={}", data));

The test works as expected, producing the following output:

data=Tyrannosaurus Rex
data=Triceratops
data=Velociraptor
data=Stegosaurus
data=Spinosaurus

Neat! Let’s look at our Iter and IterMut implementations again because they still don’t
return &T or &mut T the way we want. Unlike into_iter(), the iter() and iter_mut()
54 CHAPTER 3 Code flow

methods on LinkedList don’t consume self; they take references to self (&self and
&mut self, respectively), which makes things quite tricky.
In stable Rust, RefCell doesn’t provide a way to get a plain reference to the object
it holds. The Ref and RefMut wrappers provide a leak() method in Rust nightly, but
let’s try to do it without using that feature.
Unfortunately, the only way to do what we want is to use unsafe. If you look at
Rust’s collection library implementations, you’ll see that they use unsafe in various
places, such as the internal implementation of next() from the Iterator trait.
We need to update the Iter and IterMut structs to include a lifetime 'a for the
reference we’re returning. We’ll also store a copy of the pointer to the data we’re
returning so that it exists as long as the iterator is in scope. We use a PhantomData field
to capture the lifetime 'a in the struct:

struct Iter<'a, T> {


next: Option<ListItemPtr<T>>,
data: Option<ItemData<T>>,
phantom: PhantomData<&'a T>,
}
struct IterMut<'a, T> {
next: Option<ListItemPtr<T>>,
data: Option<ItemData<T>>,
phantom: PhantomData<&'a T>,
}

Lifetimes
Lifetimes ensure that references are valid for a certain period to prevent dangling ref-
erences (akin to dangling pointers in C or C++). Rust introduced the concept of life-
times to allow the compiler’s borrow checker to verify that references are valid at
compile time and give programmers a way to communicate this information to the
compiler. Lifetimes are denoted by an apostrophe (') followed by a name, such as
'a, 'b, and 'c.

Rust’s lifetimes are a bit tricky to grok at first, but with practice, you’ll see that they’re
quite simple. Here are a few important points to consider regarding lifetimes:
 A variable’s lifetime is the period for which it’s valid, beginning when the vari-
able is created and ending when it is destroyed.
 A reference is valid for the lifetime 'a, where 'a is an arbitrary name that car-
ries no meaning other than to identify the lifetime.
 A reference is valid for the lifetime of the object it references or the lifetime of
the scope in which it was created, whichever is shorter.
 Sometimes, we have to define lifetimes explicitly to help the compiler under-
stand the relationship between references. At other times, the compiler can
infer the lifetimes for us (generally the default).
 If the compiler can’t infer the lifetimes, it produces an error message, and
you’ll need to provide the lifetimes explicitly.
3.2 Functional Rust 55

 Lifetimes always exist in the context of a reference and are always associated
with a reference. You don’t need a lifetime if you don’t have a reference, and
the compiler will infer a lifetime for you if you don’t define one explicitly.
Lifetimes are generally introduced at the function, struct, or trait level. Where the life-
time is introduced determines the scope of the lifetime. If you introduce a lifetime at
the function level, the lifetime is valid for the duration of the function (or struct, trait,
or so on). Consider the following small program, which introduces the functions
print_without_lifetime() and print_with_lifetime():

fn print_without_lifetime(s: &str) {
println!("{}", s);
}

fn print_with_lifetime<'a>(s: &'a str) {


println!("{}", s);
}

fn main() {
print_without_lifetime("calling print_without_lifetime()");
print_with_lifetime("calling print_with_lifetime()");
}

The two functions are identical except that print_with_lifetime() has an explicit
lifetime 'a defined for the reference to the string s. The compiler will infer the lifetime
for print_without_lifetime(), but we explicitly define the lifetime for print_
with_lifetime().

Adding the lifetime 'a to the function signature tells the compiler that the reference
is valid for the duration of the function, which in this case is simply the duration of
the function call.
If you were to add a lifetime to the definition of a struct instead, the lifetime would
be valid for the duration of the struct object. Consider the following example:
struct RefStruct<'a> {
s_ref: &'a str,
}

fn main() {
let dog = "dog"; dog_struct must
let dog_struct = RefStruct { s_ref: dog };
not outlive dog.
println!("I am a {}", dog_struct.s_ref)
}

In this code, the lifetime 'a is introduced at the struct level, which means that the
reference s_ref is valid for the duration of the struct RefStruct. Now we can put a
reference to dog in the struct RefStruct and print it as long as dog outlives dog_
struct.

If this concept doesn’t make complete sense just yet, don’t worry; it will become
more apparent as you spend more time with Rust. For more information on lifetimes,
see the section on lifetimes at [Link]
56 CHAPTER 3 Code flow

We also need to initialize the new data and phantom fields in iter() and iter_mut():
impl<T> LinkedList<T> {
fn iter(&self) -> Iter<T> {
Iter {
next: Some([Link]()),
data: None,
phantom: PhantomData,
}
}
fn iter_mut(&mut self) -> IterMut<T> {
IterMut {
next: Some([Link]()),
data: None,
phantom: PhantomData,
}
}
}

Now we can implement the next() method for both:


impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
match [Link]() {
Some(ptr) => {
[Link] = ptr.as_ref().borrow().[Link]();
[Link] = Some(ptr.as_ref().borrow().[Link]());
unsafe { Some(&*[Link].as_ref().unwrap().as_ptr()) }
}
None => None,
}
}
}
impl<'a, T> Iterator for IterMut<'a, T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
match [Link]() {
Some(ptr) => {
[Link] = ptr.as_ref().borrow().[Link]();
[Link] = Some(ptr.as_ref().borrow().[Link]());
unsafe { Some(&mut *[Link].as_ref().unwrap().as_ptr()) }
}
None => None,
}
}
}

As you can see, we’ve got to do some pointer coercion to get what we want. We use
the as_ptr() method on RefCell to get *mut T; next, we dereference that pointer;
then we take another reference. This approach isn’t pretty, but it works. Keep in
mind that this structure isn’t thread-safe. Finally, we can test it, and the code prints
what we expect:
3.2 Functional Rust 57

let mut dinosaurs = LinkedList::new("Tyrannosaurus Rex");


[Link]("Triceratops");
[Link]("Velociraptor");
[Link]("Stegosaurus");
[Link]("Spinosaurus");
dinosaurs
.iter()
.for_each(|data| println!("data={}", data));

dinosaurs
.iter_mut()
.for_each(|data| println!("data={}", data));

One more thing: we need to add the IntoIterator trait and remove the previous
impl<T> Iterator for LinkedList<T> {} block. By doing so, we can iterate over
our list by using a for loop:

impl<'a, T> IntoIterator for &'a LinkedList<T> {


type IntoIter = Iter<'a, T>;
type Item = &'a T;
fn into_iter(self) -> Self::IntoIter {
Wraps iter()
[Link]()
on LinkedList
}
}
impl<'a, T> IntoIterator for &'a mut LinkedList<T> {
type IntoIter = IterMut<'a, T>;
type Item = &'a mut T;
fn into_iter(self) -> Self::IntoIter { Wraps iter_mut()
self.iter_mut()
on LinkedList
}
} We don’t need the 'a
impl<T> IntoIterator for LinkedList<T> { lifetime parameter here
type IntoIter = IntoIter<T>; because it’s not used later.
type Item = T;
fn into_iter(self) -> Self::IntoIter {
self.into_iter()
Wraps into_iter()
} on LinkedList
}

We can test the code as follows, using a plain old for loop:

for data in &linked_list {


println!("with for loop: data={}", data);
}

The compiler knows which implementation of IntoIterator to use based on the type
passed to the for loop. In this case, we’re passing &linked_list, so the compiler uses
the form that returns &T, calling the iter() method on LinkedList.
When you have iterators implemented, they unlock a lot of built-in functionality,
including for_each(), map(), reduce(), filter(), zip(), and fold(). You can also
use for … {} with structures that implement IntoIterator or Iterator.
58 CHAPTER 3 Code flow

NOTE I generally prefer using the for_each() method as opposed to the for
… {} loop syntax, although these approaches are functionally equivalent.
for_each() accepts a function as its argument, which means that you can pass
a closure or another function to it directly. In special cases, such as when
you’re using async/await, you must use a for loop rather than for_each().

3.2.5 Iterator features


Let’s take a quick tour of the features that iterators unlock. Here’s an example of
map():

let arr = [1, 2, 3, 4];


println!("{:?}", arr);
let vec: Vec<_> = [Link]().map(|v| v.to_string()).collect();
println!("{:?}", vec);

First, we initialize an array with some integers. Next, we convert our integers to strings
of integers (that is, print them to a string). To do that, we map each value to a string
by using map(). map() takes a function as its argument; it’s a higher-order function.
Let’s take a quick look at the signature of map():

fn map<B, F>(self, f: F) -> Map<Self, F>


where
F: FnMut(Self::Item) -> B,
{ ... }

The map() method takes a function with one parameter, Self::Item, as noted by the
trait bounds. If you recall from the Iterator trait, Self::Item is defined by the iterator
itself. In the case of a slice, array, or Vec, Self::Item is &T. That function can return any
type, denoted by the B generic parameter. What’s most interesting about map() is that it
merely returns another iterator, this time a special one called Map that Rust provides.
We pass a closure to map(), but we could also supply the i32::to_string() function
directly as an argument.
TIP Rust’s iterators use lazy evaluation when possible, such as with map().
The results are not computed until you force evaluation (such as by calling
collect()).

The last step is calling collect(), which converts an iterator to a collection—usually,


a Vec. You’ll notice that we have to tell the compiler what the target type is because it
can’t figure out the type automatically. Running the preceding code produces the fol-
lowing output:

[1, 2, 3, 4]
["1", "2", "3", "4"]

Suppose that we want to do something slightly more elaborate. Perhaps we want to


convert a Vec to a LinkedList from the Rust standard library while also applying a
3.2 Functional Rust 59

transformation. Let’s reuse the second vec from the preceding example and parse our
strings back into integers:

let linkedlist: LinkedList<i32> =


[Link]().flat_map(|v| [Link]::<i32>()).collect();
println!("{:?}", linkedlist);

We did something new by using flat_map() instead of map(). Why are we using
flat_map()? Because String::parse() returns a Result, so we need to flatten the
result of that parsing operation. We could call unwrap() after parsing, but flat_map()
is a little cleaner, and it handles errors somewhat gracefully (by tossing them aside).
To elaborate, flat_map() flattens the Result by calling the Result::into_iter()
method, which returns an iterator over the Ok value if it’s present or an empty iterator
if it’s not. The Err value is ignored when the Result is flattened.
The problem is that if our parsing contains an error, we might not catch it. Not to
worry. partition() has our back:

let arr = ["duck", "1", "2", "goose", "3", "4"];


let (successes, failures): (Vec<_>, Vec<_>) = arr
.iter()
.map(|v| [Link]::<i32>())
.partition(Result::is_ok);
println!("successses={:?}", successes);
println!("failures={:?}", failures);

Here, we’re taking a list of strings and trying to parse each string into an integer.
Because we managed to get a duck and a goose in there (they aren’t integers), parsing
them will fail. We want to split, or partition, the result of the parsing job, so we’re going
to partition on Result::is_ok(), which returns true if the result is Ok. Running the
preceding code prints the following:

successses=[Ok(1), Ok(2), Ok(3), Ok(4)]


failures=[Err(ParseIntError { kind: InvalidDigit }),
Err(ParseIntError { kind: InvalidDigit })]

That’s odd—our successes and failures are still wrapped in a Result, which makes
sense because we didn’t unwrap them. We can unwrap them with another step:

let successes: Vec<_> =


successes.into_iter().map(Result::unwrap).collect();
let failures: Vec<_> =
failures.into_iter().map(Result::unwrap_err).collect();
println!("successses={:?}", successes);
println!("failures={:?}", failures);

Notice that we’re calling into_iter() on our Vec because when we unwrap the
Result, we also want to consume it. into_iter(), if you recall, consumes the Vec and
its contents. Running the preceding code produces the following:
60 CHAPTER 3 Code flow

successses=[1, 2, 3, 4]
failures=[ParseIntError { kind: InvalidDigit },
ParseIntError { kind: InvalidDigit }]

Sweet! Everything is as it should be.

TIP Try to avoid using constructs such as for and while loops; instead, use
collections with iterators. Instead of a for loop, you can use for_each(), and
instead of a while loop, you can use map_while().

We can get quite elaborate in chaining operations with iterators. Rust also provides a
few special-purpose iterators to handle more complex tasks, such as counting with
Enumerate. Here’s an example that shows how we might use Enumerate with a list of
dog breeds:

let popular_dog_breeds = vec![


"Labrador",
"French Bulldog",
"Golden Retriever",
"German Shepherd",
"Poodle",
"Bulldog",
"Beagle",
"Rottweiler",
"Pointer",
"Dachshund",
];

let ranked_breeds: Vec<_> =


popular_dog_breeds.into_iter().enumerate().collect();

println!("{:?}", ranked_breeds);

Running this code yields the following output:

[(0, "Labrador"), (1, "French Bulldog"), (2, "Golden Retriever"),


(3, "German Shepherd"), (4, "Poodle"), (5, "Bulldog"), (6, "Beagle"),
(7, "Rottweiler"), (8, "Pointer"), (9, "Dachshund")]

That’s close but probably not quite what we want. It would make sense to start the count
at 1 instead of 0. With a small change, we can improve the code to get the result we’re
looking for:

let ranked_breeds: Vec<_> = popular_dog_breeds


.into_iter()
.enumerate()
.map(|(idx, breed)| (idx + 1, breed))
.collect();

We added a map() after enumerate() to unpack the tuple produced by enumerate()


and return it with 1 added to the index. Now we get the result we want:
Summary 61

[(1, "Labrador"), (2, "French Bulldog"), (3, "Golden Retriever"),


(4, "German Shepherd"), (5, "Poodle"), (6, "Bulldog"), (7, "Beagle"),
(8, "Rottweiler"), (9, "Pointer"), (10, "Dachshund")]

What if we want to count down instead of up? We can reverse the list with rev():
let ranked_breeds: Vec<_> = popular_dog_breeds
.into_iter()
.enumerate()
.map(|(idx, breed)| (idx + 1, breed))
.rev()
.collect();

Iterators are among my favorite abstractions in Rust. It’s remarkable how quickly you
can go from a quick-and-dirty data structure to a full-featured collection simply by
implementing a few iterator traits.
TIP For a complete list of all features provided by Rust’s iterators, consult the
standard library reference at [Link]
Between iterators and closures, Rust provides what you need to write purely functional
code easily. Rust’s memory model does make it trickier to perform specific tasks in
Rust that may be trivial in other languages, but almost no other language can compete
with Rust in terms of features, safety, and performance.

Summary
 Pattern matching allows us to unpack data structures and handle a variety of sce-
narios in a much cleaner way than using combinations of if/else statements.
 We can use pattern matching with the ? operator to handle errors gracefully
and unwrap or destructure values.
 We can destructure nested structs and enums when pattern matching, and we
can also match on values.
 Rust encourages functional programming patterns, particularly with closures
and iterators. Learning these patterns will help you use Rust effectively.
 Iterators use a fluent interface, and along with closures, we can easily express
operations and mutations on data structures.
 Iterators typically hold a reference to the data (such as borrowed data) or use a
move to move the items out of the underlying sequence.
 Usually, the iter() method returns an iterator with references, and into_iter()
gives us an iterator that takes ownership with a move.

You might also like