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:
parent
3b7442a498
commit
9f9f845acc
@ -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
3
src/closures.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Closures
|
||||
|
||||
{{%segment outline}}
|
48
src/closures/capturing.md
Normal file
48
src/closures/capturing.md
Normal 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
13
src/closures/exercise.md
Normal 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
69
src/closures/exercise.rs
Normal 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
5
src/closures/solution.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Solution
|
||||
|
||||
```rust,editable
|
||||
{{#include exercise.rs:solution}}
|
||||
```
|
32
src/closures/syntax.md
Normal file
32
src/closures/syntax.md
Normal 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
72
src/closures/traits.md
Normal 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>
|
@ -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>
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user