1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-03-20 14:31:15 +02:00

Break closures into its own segment (#2574)

In teaching the course last week, we broke here, partly due to time
constraints, but partly because this is a pretty mind-bending topic to
tackle at the end of an information-dense day. A break helps, and
spreading the content over a few slides helps as well.

By the timings in the course, this leaves day 2 looking like

*Fundamentals // Day 2 Morning*
_1 hour and 55 minutes: (1 hour and 10 minutes short)_

* Welcome - _3 minutes_
* Pattern Matching - _45 minutes_
* Methods and Traits - _50 minutes_

*Fundamentals // Day 2 Afternoon*
_3 hours and 30 minutes ( *30 minutes too long*)_

* Welcome - _0 minutes_
* Generics - _45 minutes_
* Standard Library Types - _1 hour_
* Standard Library Traits - _1 hour_
* Closures - _20 minutes_

Maybe we should move generics to the morning session?
This commit is contained in:
Dustin J. Mitchell 2025-01-23 03:32:59 -05:00 committed by GitHub
parent 3b7442a498
commit 9f9f845acc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 253 additions and 85 deletions

View File

@ -95,10 +95,6 @@
- [Deriving](methods-and-traits/deriving.md)
- [Exercise: Generic Logger](methods-and-traits/exercise.md)
- [Solution](methods-and-traits/solution.md)
# Day 2: Afternoon
- [Welcome](welcome-day-2-afternoon.md)
- [Generics](generics.md)
- [Generic Functions](generics/generic-functions.md)
- [Generic Data Types](generics/generic-data.md)
@ -108,6 +104,10 @@
- [`dyn Trait`](generics/dyn-trait.md)
- [Exercise: Generic `min`](generics/exercise.md)
- [Solution](generics/solution.md)
# Day 2: Afternoon
- [Welcome](welcome-day-2-afternoon.md)
- [Standard Library Types](std-types.md)
- [Standard Library](std-types/std.md)
- [Documentation](std-types/docs.md)
@ -125,9 +125,14 @@
- [Casting](std-traits/casting.md)
- [`Read` and `Write`](std-traits/read-and-write.md)
- [`Default`, struct update syntax](std-traits/default.md)
- [Closures](std-traits/closures.md)
- [Exercise: ROT13](std-traits/exercise.md)
- [Solution](std-traits/solution.md)
- [Closures](closures.md)
- [Closure Syntax](closures/syntax.md)
- [Capturing](closures/capturing.md)
- [Closure Traits](closures/traits.md)
- [Exercise: Log Filter](closures/exercise.md)
- [Solution](closures/solution.md)
---

3
src/closures.md Normal file
View File

@ -0,0 +1,3 @@
# Closures
{{%segment outline}}

48
src/closures/capturing.md Normal file
View File

@ -0,0 +1,48 @@
---
minutes: 5
---
# Capturing
A closure can capture variables from the environment where it was defined.
```rust,editable
fn main() {
let max_value = 5;
let clamp = |v| {
if v > max_value {
max_value
} else {
v
}
};
println!(
"clamped values at {max_value}: {:?}",
(0..10).map(clamp).collect::<Vec<_>>()
);
}
```
<details>
- By default, a closure captures values by reference. Here `max_value` is
captured by `clamp`, but still available to `main` for printing. Try making
`max_value` mutable, changing it, and printing the clamped values again. Why
doesn't this work?
- If a closure mutates values, it will capture them by mutable reference. Try
adding `max_value += 1` to `clamp`.
- You can force a closure to move values instead of referencing them with the
`move` keyword. This can help with lifetimes, for example if the closure must
outlive the captured values (more on lifetimes later).
This looks like `move |v| ..`. Try adding this keyword and see if `main` can
still access `max_value` after defining `clamp`.
- By default, closures will capture each variable from an outer scope by the
least demanding form of access they can (by shared reference if possible, then
exclusive reference, then by move). The `move` keyword forces capture by
value.
</details>

13
src/closures/exercise.md Normal file
View File

@ -0,0 +1,13 @@
# Exercise: Log Filter
Building on the generic logger from this morning, implement a `Filter` which
uses a closure to filter log messages, sending those which pass the filtering
predicate to an inner logger.
```rust,compile_fail
{{#include exercise.rs:setup}}
// TODO: Define and implement `Filter`.
{{#include exercise.rs:main}}
```

69
src/closures/exercise.rs Normal file
View File

@ -0,0 +1,69 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ANCHOR: solution
// ANCHOR: setup
pub trait Logger {
/// Log a message at the given verbosity level.
fn log(&self, verbosity: u8, message: &str);
}
struct StderrLogger;
impl Logger for StderrLogger {
fn log(&self, verbosity: u8, message: &str) {
eprintln!("verbosity={verbosity}: {message}");
}
}
// ANCHOR_END: setup
/// Only log messages matching a filtering predicate.
struct Filter<L, P>
where
L: Logger,
P: Fn(u8, &str) -> bool,
{
inner: L,
predicate: P,
}
impl<L, P> Filter<L, P>
where
L: Logger,
P: Fn(u8, &str) -> bool,
{
fn new(inner: L, predicate: P) -> Self {
Self { inner, predicate }
}
}
impl<L, P> Logger for Filter<L, P>
where
L: Logger,
P: Fn(u8, &str) -> bool,
{
fn log(&self, verbosity: u8, message: &str) {
if (self.predicate)(verbosity, message) {
self.inner.log(verbosity, message);
}
}
}
// ANCHOR: main
fn main() {
let logger = Filter::new(StderrLogger, |_verbosity, msg| msg.contains("yikes"));
logger.log(5, "FYI");
logger.log(1, "yikes, something went wrong");
logger.log(2, "uhoh");
}
// ANCHOR_END: main

5
src/closures/solution.md Normal file
View File

@ -0,0 +1,5 @@
# Solution
```rust,editable
{{#include exercise.rs:solution}}
```

32
src/closures/syntax.md Normal file
View File

@ -0,0 +1,32 @@
---
minutes: 3
---
# Closure Syntax
Closures are created with vertical bars: `|..| ..`.
```rust,editable
fn main() {
let value = Some(13);
dbg!(value.map(|num| format!("{num}")));
let mut nums = vec![1, 10, 99, 24];
// Sort even numbers first.
nums.sort_by_key(|v| if v % 2 == 0 { (0, *v) } else { (1, *v) });
dbg!(nums);
}
```
<details>
- The arguments go between the `|..|`. The body can be surrounded by `{ .. }`,
but if it is a single expression these can be omitted.
- Argument types are optional, and are inferred if not given. The return type is
also optional, but can only be written if using `{ .. }` around the body.
- The examples are both lambdas -- they do not capture anything from their
environment. We will see captures next.
</details>

72
src/closures/traits.md Normal file
View File

@ -0,0 +1,72 @@
---
minutes: 10
---
# Closures
Closures or lambda expressions have types which cannot be named. However, they
implement special [`Fn`](https://doc.rust-lang.org/std/ops/trait.Fn.html),
[`FnMut`](https://doc.rust-lang.org/std/ops/trait.FnMut.html), and
[`FnOnce`](https://doc.rust-lang.org/std/ops/trait.FnOnce.html) traits:
The special type `fn` refers to function pointers - either the address of a
function, or a closure that captures nothing.
```rust,editable
fn apply_and_log(func: impl FnOnce(String) -> String, func_name: &str, input: &str) {
println!("Calling {func_name}({input}): {}", func(input.to_string()))
}
fn main() {
let suffix = "-itis";
let add_suffix = |x| format!("{x}{suffix}");
apply_and_log(&add_suffix, "add_suffix", "senior");
apply_and_log(&add_suffix, "add_suffix", "appenix");
let mut v = Vec::new();
let mut accumulate = |x| {
v.push(x);
v.join("/")
};
apply_and_log(&mut accumulate, "accumulate", "red");
apply_and_log(&mut accumulate, "accumulate", "green");
apply_and_log(&mut accumulate, "accumulate", "blue");
let take_and_reverse = |mut prefix: String| {
prefix.push_str(&v.into_iter().rev().collect::<Vec<_>>().join("/"));
prefix
};
apply_and_log(take_and_reverse, "take_and_reverse", "reversed: ");
}
```
<details>
An `Fn` (e.g. `add_suffix`) neither consumes nor mutates captured values. It can
be called needing only a shared reference to the closure, which means the
closure can be executed repeatedly and even concurrently.
An `FnMut` (e.g. `accumulate`) might mutate captured values. The closure object
is accessed via exclusive reference, so it can be called repeatedly but not
concurrently.
If you have an `FnOnce` (e.g. `take_and_reverse`), you may only call it once.
Doing so consumes the closure and any values captured by move.
`FnMut` is a subtype of `FnOnce`. `Fn` is a subtype of `FnMut` and `FnOnce`.
I.e. you can use an `FnMut` wherever an `FnOnce` is called for, and you can use
an `Fn` wherever an `FnMut` or `FnOnce` is called for.
When you define a function that takes a closure, you should take `FnOnce` if you
can (i.e. you call it once), or `FnMut` else, and last `Fn`. This allows the
most flexibility for the caller.
In contrast, when you have a closure, the most flexible you can have is `Fn`
(which can be passed to a consumer of any of the 3 closure traits), then
`FnMut`, and lastly `FnOnce`.
The compiler also infers `Copy` (e.g. for `add_suffix`) and `Clone` (e.g.
`take_and_reverse`), depending on what the closure captures. Function pointers
(references to `fn` items) implement `Copy` and `Fn`.
</details>

View File

@ -1,80 +0,0 @@
---
minutes: 10
---
# Closures
Closures or lambda expressions have types which cannot be named. However, they
implement special [`Fn`](https://doc.rust-lang.org/std/ops/trait.Fn.html),
[`FnMut`](https://doc.rust-lang.org/std/ops/trait.FnMut.html), and
[`FnOnce`](https://doc.rust-lang.org/std/ops/trait.FnOnce.html) traits:
```rust,editable
fn apply_and_log(func: impl FnOnce(i32) -> i32, func_name: &str, input: i32) {
println!("Calling {func_name}({input}): {}", func(input))
}
fn main() {
let n = 3;
let add_3 = |x| x + n;
apply_and_log(&add_3, "add_3", 10);
apply_and_log(&add_3, "add_3", 20);
let mut v = Vec::new();
let mut accumulate = |x: i32| {
v.push(x);
v.iter().sum::<i32>()
};
apply_and_log(&mut accumulate, "accumulate", 4);
apply_and_log(&mut accumulate, "accumulate", 5);
let multiply_sum = |x| x * v.into_iter().sum::<i32>();
apply_and_log(multiply_sum, "multiply_sum", 3);
}
```
<details>
An `Fn` (e.g. `add_3`) neither consumes nor mutates captured values. It can be
called needing only a shared reference to the closure, which means the closure
can be executed repeatedly and even concurrently.
An `FnMut` (e.g. `accumulate`) might mutate captured values. The closure object
is accessed via exclusive reference, so it can be called repeatedly but not
concurrently.
If you have an `FnOnce` (e.g. `multiply_sum`), you may only call it once. Doing
so consumes the closure and any values captured by move.
`FnMut` is a subtype of `FnOnce`. `Fn` is a subtype of `FnMut` and `FnOnce`.
I.e. you can use an `FnMut` wherever an `FnOnce` is called for, and you can use
an `Fn` wherever an `FnMut` or `FnOnce` is called for.
When you define a function that takes a closure, you should take `FnOnce` if you
can (i.e. you call it once), or `FnMut` else, and last `Fn`. This allows the
most flexibility for the caller.
In contrast, when you have a closure, the most flexible you can have is `Fn`
(which can be passed to a consumer of any of the 3 closure traits), then
`FnMut`, and lastly `FnOnce`.
The compiler also infers `Copy` (e.g. for `add_3`) and `Clone` (e.g.
`multiply_sum`), depending on what the closure captures. Function pointers
(references to `fn` items) implement `Copy` and `Fn`.
By default, closures will capture each variable from an outer scope by the least
demanding form of access they can (by shared reference if possible, then
exclusive reference, then by move). The `move` keyword forces capture by value.
```rust,editable
fn make_greeter(prefix: String) -> impl Fn(&str) {
return move |name| println!("{} {}", prefix, name);
}
fn main() {
let hi = make_greeter("Hi".to_string());
hi("Greg");
}
```
</details>

View File

@ -15,6 +15,7 @@ system:
- Traits: behaviors shared by multiple types.
- Generics: parameterizing types on other types.
- Standard library types and traits: a tour of Rust's rich standard library.
- Closures: function pointers with data.
## Schedule