03 Code Flow
03 Code Flow
34
3.1 A tour of pattern matching 35
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.
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.
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.
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
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:
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:
|
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:
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:
|
= note: expected reference `&T`
found reference `&'static str`
For more information about this error, try `rustc --explain E0308`.
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"),
}
}
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()),
}
}
fn write_to_file_without_result() {
use std::fs::File;
use std::io::prelude::*;
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,
}
Now we can update our file-writing code to use our error type by returning Error-
Wrapper in our write_to_file() function:
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
}
}
}
crates into your error-handling code. Chapter 4 revisits the ? operator and error han-
dling in Rust.
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:
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:
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
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:
|
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.
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.
use std::cell::RefCell;
use std::rc::Rc;
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,
}
}
}
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 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).
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:
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:
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.
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:
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
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:
}
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,
}
}
}
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:
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:
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:
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 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,
}
}
}
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
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:
We can test the code as follows, using a plain old for loop:
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().
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():
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()).
[1, 2, 3, 4]
["1", "2", "3", "4"]
transformation. Let’s reuse the second vec from the preceding example and parse our
strings back into integers:
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:
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:
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:
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 }]
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:
println!("{:?}", ranked_breeds);
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:
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.