Start of better Fn traits explanation

This commit is contained in:
Carol (Nichols || Goulding) 2022-04-22 21:03:52 -04:00
parent ce4da5ccbf
commit 5e58b9cc3f
No known key found for this signature in database
GPG Key ID: E907EE5A736F87D4
4 changed files with 266 additions and 16 deletions

View File

@ -60,6 +60,7 @@ BoxMeUp
BTreeSet
BuildHasher
Cacher
cacher
Cagain
callsite
CamelCase

View File

@ -7,6 +7,11 @@ closures can capture values from the scope in which theyre defined. Well
demonstrate how these closure features allow for code reuse and behavior
customization.
<!-- Old headings. Do not remove or links may break. -->
<a id="creating-an-abstraction-of-behavior-with-closures"></a>
<a id="refactoring-using-functions"></a>
<a id="refactoring-with-closures-to-store-code"></a>
### Capturing the Environment with Closures
The first aspect of closures we're going to examine is that closures can
@ -224,8 +229,6 @@ closure definition and the closure 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!
If you want to force the closure to take ownership of the values it uses in the
environment even though the body of the closure doesn't strictly need
ownership, you can use the `move` keyword before the parameter list. This
@ -233,13 +236,221 @@ technique is mostly useful when passing a closure to a new thread to move the
data so its owned by the new thread. Well have more examples of `move`
closures in Chapter 16 when we talk about concurrency.
### Creating an Abstraction of Behavior with Closures
<!-- Old headings. Do not remove or links may break. -->
<a id="storing-closures-using-generic-parameters-and-the-fn-traits"></a>
<a id="limitations-of-the-cacher-implementation"></a>
#### Refactoring Using Functions
### Moving Captured Values Out of the Closure and the `Fn` Traits
#### Refactoring with Closures to Store Code
Once a closure has captured a reference or moved a value into the closure, the
code in the body of the function also affects what happens to the references or
values as a result of calling the function. A closure body can move a captured
value out of the closure, can mutate the captured value, can neither move nor
mutate the captured value, or can capture nothing from the environment. The way
a closure captures and handles values from the environment affects which traits
the closure implements. The traits are how functions and structs can specify
what kinds of closures they can use.
### Storing Closures Using Generic Parameters and the `Fn` Traits
Closures will automatically implement one, two, or all three of these `Fn`
traits, in an additive fashion:
1. `FnOnce` applies to closures that can be called at least once. All closures
implement this trait, because all closures can be called. If a closure moves
captured values out of its body, then that closure only implements `FnOnce`
and not any of the other `Fn` traits, because it can only be called once.
2. `FnMut` applies to closures that don't move captured values out of their
body, but that might mutate the captured values. These closures can be
called more than once.
3. `Fn` applies to closures that don't move captured values out of their body
and that don't mutate captured values. 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. Closures that don't
capture anything from their environment implement `Fn`.
> Note: Functions can implement all three of the `Fn` traits too. If what we
> want to do doesnt require capturing a value from the environment, we can use
> a function rather than a closure where we need something that implements one
> of the `Fn` traits.
Let's look at the definition of the `unwrap_or_else` method on `Option<T>` that
we used in Listing 13-x:
```rust,ignore
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 the
`unwrap_or_else` function: code that calls `unwrap_or_else` on an
`Option<String>`, for example, will get a `String`.
Next, notice that the `unwrap_or_else` function has an additional generic type
parameter, `F`. The `F` type is the type of the parameter named `f`, which is
the closure we provide when calling `unwrap_or_else`.
The trait bound specified on the generic type `F` is `FnOnce() -> T`, which
means `F` must be able to be called at least once, take no arguments, and
return a `T`. Using `FnOnce` in the trait bound expresses the constraint that
`unwrap_or_else` is only going to call `f` at most one time. In the body of
`unwrap_or_else`, we can see that if the `Option` is `Some`, `f` won't be
called. If the `Option` is `None`, `f` will be called once. Because all
closures implement `FnOnce`, `unwrap_or_else` accepts the most different kinds
of closures and is as flexible as it can be.
Now let's look at the standard library method, `sort_by_key` defined on slices,
that takes a closure that implements `FnMut` to see how that differs. The
`sort_by_key` method takes a closure that 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 function is useful when you want to sort a slice by a
particular attribute of each item. In Listing 13-x, we have a list of
`Rectangle` instances and we use `sort_by_key` to order them by their `width`
attribute from low to high:
```rust
#[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);
}
```
This code prints:
```console
[
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
Rectangle {
width: 10,
height: 1,
},
]
```
The reason `sort_by_key` is defined to take an `FnMut` closure is that it calls
the closure multiple times: once for each item in the slice. The closure `|r|
r.width` doesn't capture, mutate, or move out anything from its environment, so
it meets the trait bound requirements.
In contrast, here's an example of a closure that only implements `FnOnce`
because it moves a value out of the environment. The compiler won't let us use
this closure with `sort_by_key`:
```rust,ignore,does_not_compile
#[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("by key called");
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 and count the
number of times `sort_by_key` gets called when sorting `list`. This code
attempts to do this counting by pushing `value`, a `String` from the closure's
environment, into the `sort_operations` vector. The closure captures `value`
then moves `value` out of the closure by transferring ownership of `value` to
the `sort_operations` vector. 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`. When we try to compile this code, we get this error
that `value` can't be moved out of the closure because the closure must
implement `FnMut`:
```console
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
--> src/main.rs:18:30
|
15 | let value = String::from("by key called");
| ----- captured outer variable
16 |
17 | list.sort_by_key(|r| {
| ______________________-
18 | | sort_operations.push(value);
| | ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait
19 | | r.width
20 | | });
| |_____- captured by this `FnMut` closure
```
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. If we're interested in the number of times
`sort_by_key` is called, keeping a counter in the environment and incrementing
its value in the closure body is a more straightforward way to calculate that.
This closure works with `sort_by_key` because it is only capturing a mutable
reference to the `num_sort_operations` counter and can therefore be called more
than once:
```rust
#[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 num_sort_operations = 0;
list.sort_by_key(|r| {
num_sort_operations += 1;
r.width
});
println!("{:#?}, sorted in {num_sort_operations} operations", list);
}
```
The `Fn` traits are important when defining or using functions or types that
make use of closures. The next section discusses iterators, and many iterator
methods take closure arguments. Keep these details of closures in mind as we
explore iterators!
[unwrap-or-else]: ../std/option/enum.Option.html#method.unwrap_or_else

View File

@ -181,3 +181,42 @@ 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.
### Using Closures that Capture Their Environment
Now that weve introduced iterators, we can demonstrate a common use of
closures that capture their environment by using the `filter` iterator adaptor.
The `filter` method on an iterator takes a closure that takes each item from
the iterator and returns a Boolean. If the closure returns `true`, the value
will be included in the iterator produced by `filter`. If the closure returns
`false`, the value wont be included in the resulting iterator.
In Listing 13-19, we use `filter` with a closure that captures the `shoe_size`
variable from its environment to iterate over a collection of `Shoe` struct
instances. It will return only shoes that are the specified size.
<span class="filename">Filename: src/lib.rs</span>
```rust,noplayground
{{#rustdoc_include ../listings/ch13-functional-features/listing-13-19/src/lib.rs}}
```
<span class="caption">Listing 13-19: Using the `filter` method with a closure
that captures `shoe_size`</span>
The `shoes_in_size` function takes ownership of a vector of shoes and a shoe
size as parameters. It returns a vector containing only shoes of the specified
size.
In the body of `shoes_in_size`, we call `into_iter` to create an iterator
that takes ownership of the vector. Then we call `filter` to adapt that
iterator into a new iterator that only contains elements for which the closure
returns `true`.
The closure captures the `shoe_size` parameter from the environment and
compares the value with each shoes size, keeping only shoes of the size
specified. Finally, calling `collect` gathers the values returned by the
adapted iterator into a vector thats 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.

View File

@ -214,14 +214,13 @@ so it takes the closure its given and gives it to an idle thread in the pool
to run.
Well define the `execute` method on `ThreadPool` to take a closure as a
parameter. Recall from the [“Storing Closures Using Generic Parameters and the
`Fn` Traits”][storing-closures-using-generic-parameters-and-the-fn-traits]<!--
ignore --> section in Chapter 13 that we can take closures as parameters with
three different traits: `Fn`, `FnMut`, and `FnOnce`. We need to decide which
kind of closure to use here. We know well end up doing something similar to
the standard library `thread::spawn` implementation, so we can look at what
bounds the signature of `thread::spawn` has on its parameter. The documentation
shows us the following:
parameter. Recall from the [“Moving Captured Values Out of the Closure and the
`Fn` Traits”][fn-traits]<!-- ignore --> section in Chapter 13 that we can take
closures as parameters with three different traits: `Fn`, `FnMut`, and
`FnOnce`. We need to decide which kind of closure to use here. We know well
end up doing something similar to the standard library `thread::spawn`
implementation, so we can look at what bounds the signature of `thread::spawn`
has on its parameter. The documentation shows us the following:
```rust,ignore
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
@ -679,5 +678,5 @@ of the call to `job()`, meaning other workers cannot receive jobs.
[creating-type-synonyms-with-type-aliases]:
ch19-04-advanced-types.html#creating-type-synonyms-with-type-aliases
[integer-types]: ch03-02-data-types.html#integer-types
[storing-closures-using-generic-parameters-and-the-fn-traits]:
ch13-01-closures.html#storing-closures-using-generic-parameters-and-the-fn-traits
[fn-traits]:
ch13-01-closures.html#moving-captured-values-out-of-the-closure-and-the-fn-traits