From 9f9f845acc51c13dc7b7427c9e79ebe9b2fb87a2 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 23 Jan 2025 03:32:59 -0500 Subject: [PATCH] Break closures into its own segment (#2574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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? --- src/SUMMARY.md | 15 ++++--- src/closures.md | 3 ++ src/closures/capturing.md | 48 +++++++++++++++++++++++ src/closures/exercise.md | 13 +++++++ src/closures/exercise.rs | 69 ++++++++++++++++++++++++++++++++ src/closures/solution.md | 5 +++ src/closures/syntax.md | 32 +++++++++++++++ src/closures/traits.md | 72 ++++++++++++++++++++++++++++++++++ src/std-traits/closures.md | 80 -------------------------------------- src/welcome-day-2.md | 1 + 10 files changed, 253 insertions(+), 85 deletions(-) create mode 100644 src/closures.md create mode 100644 src/closures/capturing.md create mode 100644 src/closures/exercise.md create mode 100644 src/closures/exercise.rs create mode 100644 src/closures/solution.md create mode 100644 src/closures/syntax.md create mode 100644 src/closures/traits.md delete mode 100644 src/std-traits/closures.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 673f7b0b..3f620c18 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -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) --- diff --git a/src/closures.md b/src/closures.md new file mode 100644 index 00000000..39c183ff --- /dev/null +++ b/src/closures.md @@ -0,0 +1,3 @@ +# Closures + +{{%segment outline}} diff --git a/src/closures/capturing.md b/src/closures/capturing.md new file mode 100644 index 00000000..3102c63a --- /dev/null +++ b/src/closures/capturing.md @@ -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::>() + ); +} +``` + +
+ +- 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. + +
diff --git a/src/closures/exercise.md b/src/closures/exercise.md new file mode 100644 index 00000000..af8049ca --- /dev/null +++ b/src/closures/exercise.md @@ -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}} +``` diff --git a/src/closures/exercise.rs b/src/closures/exercise.rs new file mode 100644 index 00000000..e6951257 --- /dev/null +++ b/src/closures/exercise.rs @@ -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 +where + L: Logger, + P: Fn(u8, &str) -> bool, +{ + inner: L, + predicate: P, +} + +impl Filter +where + L: Logger, + P: Fn(u8, &str) -> bool, +{ + fn new(inner: L, predicate: P) -> Self { + Self { inner, predicate } + } +} +impl Logger for Filter +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 diff --git a/src/closures/solution.md b/src/closures/solution.md new file mode 100644 index 00000000..b4a4c92c --- /dev/null +++ b/src/closures/solution.md @@ -0,0 +1,5 @@ +# Solution + +```rust,editable +{{#include exercise.rs:solution}} +``` diff --git a/src/closures/syntax.md b/src/closures/syntax.md new file mode 100644 index 00000000..65085e7f --- /dev/null +++ b/src/closures/syntax.md @@ -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); +} +``` + +
+ +- 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. + +
diff --git a/src/closures/traits.md b/src/closures/traits.md new file mode 100644 index 00000000..e634d89a --- /dev/null +++ b/src/closures/traits.md @@ -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::>().join("/")); + prefix + }; + apply_and_log(take_and_reverse, "take_and_reverse", "reversed: "); +} +``` + +
+ +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`. + +
diff --git a/src/std-traits/closures.md b/src/std-traits/closures.md deleted file mode 100644 index cdd315d2..00000000 --- a/src/std-traits/closures.md +++ /dev/null @@ -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::() - }; - apply_and_log(&mut accumulate, "accumulate", 4); - apply_and_log(&mut accumulate, "accumulate", 5); - - let multiply_sum = |x| x * v.into_iter().sum::(); - apply_and_log(multiply_sum, "multiply_sum", 3); -} -``` - -
- -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"); -} -``` - -
diff --git a/src/welcome-day-2.md b/src/welcome-day-2.md index 3c8c2d28..1cf9c40d 100644 --- a/src/welcome-day-2.md +++ b/src/welcome-day-2.md @@ -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