Chapter 12: More on closures, generics, and threads
The introduction connects the chapter’s new tools to familiar Rust concepts and gives a few high-level distinctions.
After this chapter you will be able to
- Arc and Rc comparison. Describe the introductory comparison of Arc with Rc.
- Closure bounds in function signatures. Explain why a function taking a closure must specify a closure trait, and read the closure bounds in the .all() and .map() signatures.
- Calling closures after captured moves. Describe why the example closure that consumes some_vec cannot be called again, and contrast it with the example that borrows some_vec.
- Closure trait call counts. Distinguish the call counts shown for functions taking Fn(), FnMut(), and FnOnce() closures: the first two call their closures twice, while calling the FnOnce() closure a second time would not compile.
- Captured variables without arguments. Explain why the example closures can take no arguments and return nothing while still using a variable from their surroundings.
- Closure trait hierarchy. Explain how FnOnce, FnMut, and Fn depend on one another through supertraits.
- Closure argument compatibility. Describe which closures can be passed to functions that ask for FnOnce, FnMut, or Fn, and why an Fn requirement is the most restrictive.
- Distinct closure types. Explain why two closures with the same signature cannot necessarily be passed to parameters declared with the same generic type, and describe how separate generic types F and G address this.
- Changing City Data With Closures. Explain how City’s change_city_data method uses an FnMut closure to work with its years and populations without taking ownership of the data.
- Generic type placeholders. Explain that a familiar generic uses a type placeholder such as T, whose type is decided when the program compiles.
- Trait bounds in function parameters. Explain how the generic version of gives_higher accepts more than i32 by requiring PartialOrd for comparison and Display for printing, and how impl Trait expresses similar requirements for an argument.
- Shared type for comparison. Explain why replacing both parameters of gives_higher with impl PartialOrd + Display does not work: a named generic T ties them to one type, while separate impl Trait arguments do not.
- Closure return type. Explain how to declare a closure as a function’s return value with impl Trait and how the returned value can be used.
- Atomic shared ownership. Explain why Arc is used instead of Rc when a value has multiple owners in different threads, and describe what atomic means at the level taught here.
- Shared mutable data layers. Distinguish what Arc and Mutex each contribute when threads need to share and change a value.
- Scoped thread lifetime guarantee. Explain why scoped threads can borrow non-'static data and why they do not require an explicit .join(). Contrast their lifetime with regular threads.
- Borrowing and shared mutation. Explain when the section's scoped-thread examples still need a Mutex and when ordinary mutable borrowing is enough. Describe why the scoped version does not need Arc or move for the borrowed values.
- Channel communication and flexibility. Explain that a channel lets one or more senders send information to one receiver, even when they are in different functions, structs, or threads.
- Channel endpoints and message type. Explain what mpsc means and what channel() creates: a connected Sender and Receiver that send and receive the same type T.
- Channel creation and type inference. Describe destructuring the tuple returned by channel() into sender and receiver, and explain when Rust can infer their generic type.
- Channel values and results. Explain how sending an i32 establishes the channel’s value type and how send and receive operations report success or failure.
- Blocking and immediate receive. Distinguish how .recv() waits for data from how .try_recv() checks immediately in the threaded example.
- Channel closure and errors. Explain when sending or receiving fails after the opposite endpoint is dropped, including what happens to already-sent data.
What it builds on
- Generics (chapter 5) · The earlier introduction to generics.
- Rc (chapter 11) · The earlier discussion of Rc.
- Multiple threads (chapter 11) · The earlier discussion of multiple threads.
- Types of closures (chapter 11) · The three closure traits Fn, FnMut, and FnOnce.
- The stack, the heap, pointers, and references (chapter 2) · The difference between taking a value and borrowing it.
- Closures and closures inside iterators (chapter 8) · The earlier introduction to closures.
- Closures inside of methods (chapter 8) · The earlier use of closures inside methods.
- Traits as bounds (chapter 7) · Traits used as bounds
Section by section
Introduction
The introduction connects the chapter’s new tools to familiar Rust concepts and gives a few high-level distinctions.
- impl Trait and generics: Explain that impl Trait is another way to use generics and is easier to write than regular generics.
Why it matters: This gives you a familiar starting point for the chapter’s later discussion of impl Trait. - Arc and Rc comparison: Describe the introductory comparison of Arc with Rc.
Why it matters: The comparison links a new type to Rc while identifying its key difference. - Scoped threads and channels: State the introductory descriptions of scoped threads and channels: scoped threads live only inside a scope, while channels send messages, even across threads.
Why it matters: These descriptions distinguish the two threading topics before their details appear.
Key terms: impl Trait, generics, Arc, Rc, thread-safe, scoped threads, scope, channels, messages
12.1 Closures as arguments
Introduces how function signatures specify closure arguments and uses examples to show when a closure can be called again.
- Closure bounds in function signatures: Explain why a function taking a closure must specify a closure trait, and read the closure bounds in the .all() and .map() signatures.
Why it matters: These bounds let you understand what an iterator method expects its closure to take and return. - Calling closures after captured moves: Describe why the example closure that consumes some_vec cannot be called again, and contrast it with the example that borrows some_vec.
Why it matters: Whether a closure moves or borrows a captured value affects whether the example can run again.
Key terms: Fn, FnMut, FnOnce, F, Self::Item, trait bounds, some_vec, .into_iter(), by value, by reference
12.1.1 Some simple closures
This section uses simple functions to show how often Fn, FnMut, and FnOnce closures can be called, and how closures are passed as arguments.
- Closure trait call counts: Distinguish the call counts shown for functions taking Fn(), FnMut(), and FnOnce() closures: the first two call their closures twice, while calling the FnOnce() closure a second time would not compile.
Why it matters: This is the basic difference the examples demonstrate before examining the closure traits more closely. - Captured variables without arguments: Explain why the example closures can take no arguments and return nothing while still using a variable from their surroundings.
Why it matters: Capturing explains how these short closures can use nearby data despite their empty argument lists. - Inline closures as arguments: Describe how an inline closure passed to a regular function does the same job as a named closure: the regular function calls it, executing the code inside it.
Why it matters: This helps you read calls that contain a closure directly in the argument list.
Key terms: Fn(), FnMut(), FnOnce(), capture variables, environment, arguments, closure, argument, inline
12.1.2 The relationship between FnOnce, FnMut, and Fn
Explains the supertrait relationship among Rust’s three closure traits and how it determines which closures a function can accept.
- Closure trait hierarchy: Explain how FnOnce, FnMut, and Fn depend on one another through supertraits.
Why it matters: This hierarchy explains why the three closure traits are not interchangeable in both directions. - Closure argument compatibility: Describe which closures can be passed to functions that ask for FnOnce, FnMut, or Fn, and why an Fn requirement is the most restrictive.
Why it matters: It lets you reason about which closure arguments satisfy a function’s trait requirement.
Key terms: FnOnce, FnMut, Fn, supertrait, subtrait, closure trait
12.1.3 Closures are all unique
This section explains why two closures with identical signatures still have different types and how to accept both in one function.
- Distinct closure types: Explain why two closures with the same signature cannot necessarily be passed to parameters declared with the same generic type, and describe how separate generic types F and G address this.
Why it matters: It explains a compiler error that can appear when a function accepts more than one closure.
Key terms: closure, signature, Fn, trait, F, G
12.1.4 A closure example
This section uses a City example to show how an FnMut closure can change stored data without taking ownership of it.
- Changing City Data With Closures: Explain how City’s change_city_data method uses an FnMut closure to work with its years and populations without taking ownership of the data.
Why it matters: The example shows a practical reason to pass an FnMut closure to a method.
Key terms: City, change_city_data, FnMut, Vec<u32>
12.2 impl Trait
The section briefly reviews how familiar generics use a type placeholder before introducing impl Trait as another form of generics.
- Generic type placeholders: Explain that a familiar generic uses a type placeholder such as T, whose type is decided when the program compiles.
Why it matters: This is the point of comparison for the upcoming explanation of impl Trait.
Key terms: generics, T
12.2.1 Regular generics compared to impl Trait
This section compares regular generic parameters with impl Trait parameters, explains why impl Trait cannot express a shared type for the comparison example, and identifies where impl Trait can be used.
- Trait bounds in function parameters: Explain how the generic version of gives_higher accepts more than i32 by requiring PartialOrd for comparison and Display for printing, and how impl Trait expresses similar requirements for an argument.
Why it matters: This lets you read what kinds of arguments either function signature accepts. - Shared type for comparison: Explain why replacing both parameters of gives_higher with impl PartialOrd + Display does not work: a named generic T ties them to one type, while separate impl Trait arguments do not.
Why it matters: It explains why a shorter signature is not always interchangeable with a named generic type. - Impl Trait uses and limits: State the placements for impl Trait described here and explain the stated advantage of returning a closure with it.
Why it matters: It shows both where this notation is available and why returning a closure is a useful case.
Key terms: generics, impl Trait, PartialOrd, Display, T, impl PartialOrd + Display, return type, closures, type alias
12.2.2 Returning closures with impl Trait
This section explains how a function can return a closure using impl Trait and how the caller can use that closure.
- Closure return type: Explain how to declare a closure as a function’s return value with impl Trait and how the returned value can be used.
Why it matters: Returning a closure lets a function provide behavior that its caller can use.
Key terms: impl Trait, closure signature, FnMut(i32) -> i32
12.3 Arc
This section explains why shared ownership across threads uses Arc, why changing shared data also requires Mutex, and how to use that combination with multiple threads.
- Atomic shared ownership: Explain why Arc is used instead of Rc when a value has multiple owners in different threads, and describe what atomic means at the level taught here.
Why it matters: It helps you choose the appropriate shared-ownership type for single-threaded or multithreaded code. - Shared mutable data layers: Distinguish what Arc and Mutex each contribute when threads need to share and change a value.
Why it matters: Separating ownership from changing data explains why the example uses both types rather than Arc alone. - Threaded value sharing pattern: Trace how the example gives threads access to the shared number and waits for them to finish.
Why it matters: It connects the Arc-and-Mutex combination to the section’s working multithreaded example.
Key terms: Arc, Rc, atomic reference counter, atomic operations, thread-safe, Mutex, Arc<Mutex<i32>>, clone, move, handles
12.4 Scoped threads
This section explains how scoped threads simplify short-lived concurrent work by allowing borrowed data and automatically joining threads at the end of a scope.
- Scoped thread lifetime guarantee: Explain why scoped threads can borrow non-'static data and why they do not require an explicit .join(). Contrast their lifetime with regular threads.
Why it matters: The lifetime guarantee is what makes scoped threads convenient for tasks that finish within one scope. - Scoped thread spawning pattern: Describe how to start scoped threads with thread::scope() and spawn them using the scope supplied to its closure, rather than calling thread::spawn() directly.
Why it matters: This is the practical distinction between starting a regular thread and starting a scoped one. - Borrowing and shared mutation: Explain when the section's scoped-thread examples still need a Mutex and when ordinary mutable borrowing is enough. Describe why the scoped version does not need Arc or move for the borrowed values.
Why it matters: Scoped borrowing simplifies sharing data, but it does not remove the need to coordinate changes made by multiple threads.
Key terms: scoped threads, non-'static data, .join(), thread::scope(), thread::spawn(), scope, Mutex, Arc, move, mutable borrow
12.5 Channels
Introduces channels as a simple, thread-safe way to send information to one receiver, even across threads or different parts of a program.
- Channel communication and flexibility: Explain that a channel lets one or more senders send information to one receiver, even when they are in different functions, structs, or threads.
Why it matters: This gives you the basic picture of what a channel connects and why it is useful across threads.
Key terms: channel, sender, receiver, thread-safe
12.5.1 Channel basics
This section introduces Rust’s mpsc channel as a paired sender and receiver and shows how to create the pair while letting Rust infer its message type.
- Channel endpoints and message type: Explain what mpsc means and what channel() creates: a connected Sender and Receiver that send and receive the same type T.
Why it matters: This gives you the basic picture of what communicates through a channel. - Channel creation and type inference: Describe destructuring the tuple returned by channel() into sender and receiver, and explain when Rust can infer their generic type.
Why it matters: It explains both the usual channel declaration and why an otherwise unused channel needs a type annotation.
Key terms: std::sync::mpsc, mpsc, channel(), Sender, Receiver, tuple, destructuring, generic, type inference
12.5.2 Implementing a channel
Shows how to send values through a channel, receive them across threads, and understand when channel operations wait or fail.
- Channel values and results: Explain how sending an i32 establishes the channel’s value type and how send and receive operations report success or failure.
Why it matters: It helps you interpret what a channel operation returns instead of assuming it always succeeds. - Cloned senders across threads: Describe how cloned channel endpoints let two threads send values to a receiver, and what determines the order printed.
Why it matters: Cloned endpoints allow multiple threads to pass information to one receiver. - Blocking and immediate receive: Distinguish how .recv() waits for data from how .try_recv() checks immediately in the threaded example.
Why it matters: Choosing between waiting and checking immediately changes whether the receiver gets delayed messages. - Channel closure and errors: Explain when sending or receiving fails after the opposite endpoint is dropped, including what happens to already-sent data.
Why it matters: Endpoint lifetime determines whether an empty receive waits or reports that the channel is closed.
Key terms: Sender, Receiver, Result, send(), clone, recv(), try_recv(), blocking, Ok, Err
12.6 Summary
Where this is used later
- Async basics (chapter 19) · The impl Trait syntax used to describe the hidden return type.
- Putting a Box around traits (chapter 13) · The earlier comparison of generics and impl Trait.
Concepts explained in this chapter
- FnMutFnMut is a Rust callable trait for closures and function-like values that can be invoked through mutable access and may mutate their captured environment.
- FnFn is a Rust trait for closures and callable values that can be called repeatedly through shared access without consuming or mutating their captured environment.
- closureA closure is an anonymous Rust function-like value that can capture variables from its surrounding environment and be passed as an argument.
- FnOnceFnOnce is a Rust callable trait for closures and function-like values that may consume their captured environment and therefore can be called at most once.
- mpscA channel is a communication abstraction with one or more sending endpoints and one receiving endpoint for transferring information between parts of a program.
- senderSender is a Rust standard-library channel handle that transmits values to a corresponding receiver.
- impl Traitimpl Trait is Rust syntax for specifying an opaque type constrained by one or more traits, with the concrete type selected by the compiler or function implementation.
- genericsA generic function is a Rust function parameterized by one or more type parameters and usable with multiple concrete types.
- trait boundA trait bound is a Rust constraint requiring a type to implement one or more specified traits before it can be used in a bounded context.
- ArcArc is Rust's atomically reference-counted smart pointer for sharing ownership of a value safely across threads.
- clientReceiver is a Rust standard-library channel handle that obtains values transmitted by corresponding senders.
- scoped threadA scoped thread is a thread whose lifetime is bounded by a lexical scope, allowing it to borrow non-'static data and requiring it to finish before the scope ends.
- atomicityAn atomic operation is an indivisible computer operation that appears to other threads as either fully completed or not performed.
- command-line argumentA command-line argument is a value supplied to a program when it is launched through a command-line interface.
- concrete typeA concrete type is a specific Rust type rather than an abstract type parameter or trait description.
- static lifetimeThe static lifetime is a Rust lifetime that lasts for the entire duration of the program.
- threadA thread is an independently scheduled execution path within a program that can run concurrently with other threads.
- allAn all operation returns a Boolean indicating whether every item in a collection satisfies a condition, or is truthy when no condition is specified.
- data transformationmap is an iterator adaptor that applies a transformation to each item and returns a new iterator producing the transformed items.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
