Progress pill
Learn how to code with the rust book

Functional Programming Features, Closures and Smart Pointers

Learning Rust with Bitcoin

Functional Programming Features, Closures and Smart Pointers

  • Understanding Closures
  • Closure Type Inference and Flexibility
  • Capturing Environment Variables
  • Closure Traits and Function Types
  • Working with Iterators
  • Iterator Adaptors and Consumers
  • Advanced Iterator Patterns
  • Introduction to Smart Pointers
  • The Box Smart Pointer
  • Implementing the Deref Trait
  • Custom Drop Implementation
While Rust is not a pure functional programming language, it incorporates features inspired by functional programming paradigms. These features enable developers to write concise code by leveraging concepts like closures and iterators. Rust includes these functional elements to provide flexible tools for data processing and callback mechanisms.
The functional programming features in Rust maintain the language's core principles of memory safety and zero-cost abstractions. When you use closures and iterators, you're not sacrificing performance for expressiveness – the Rust compiler optimizes these constructs to produce efficient machine code comparable to traditional loop-based approaches.

Understanding Closures

Closures in Rust are anonymous functions that can capture variables from their surrounding environment. In other programming languages, these are often called lambda functions. The key characteristic of closures is their ability to "close over" their environment, meaning they can access and use variables that exist in the scope where the closure is defined.
The syntax for closures uses pipe characters (|) instead of parentheses to define parameters. For a closure with no parameters, you write ||, and for closures with parameters, you list them between the pipes like |x, y|. If the closure body consists of a single expression, you can omit the curly braces, making the syntax very concise.
Consider this practical example of a t-shirt company that gives away exclusive shirts based on customer preferences. If a customer has specified a favorite color, they receive that color; otherwise, they get the most stocked color as a default. Using closures, this logic becomes: user_preference.unwrap_or_else(|| self.most_stocked()). The closure || self.most_stocked() provides the default value only when needed, and it can access self from its environment.

Closure Type Inference and Flexibility

One of Rust's most convenient features with closures is automatic type inference. Unlike regular functions where you must explicitly specify parameter types and return types, closures can often infer these types from context. The compiler analyzes how the closure is used and determines the appropriate types automatically. However, once a closure is called with specific types, those types become fixed for that closure instance.
You can store closures in variables just like any other value, making them first-class citizens in the language. When you assign a closure to a variable, you can call it later using parentheses: let my_closure = |x| x + 1; let result = my_closure(5);. This flexibility allows you to pass closures as arguments to functions, return them from functions, and use them in data structures.
If the compiler cannot infer types or if you want to be explicit, you can annotate closure parameters and return types using syntax similar to functions: |x: i32| -> i32 { x + 1 }. This explicit typing is sometimes necessary in complex scenarios where the compiler needs additional information to resolve types correctly.

Capturing Environment Variables

Closures can capture variables from their environment in three different ways: by immutable reference, by mutable reference, or by taking ownership. The Rust compiler automatically determines the most restrictive capture method that satisfies your closure's needs, following the principle of least privilege.
When a closure only needs to read a value, it captures by immutable reference. This allows the original variable to remain accessible after the closure is defined and called. For example, a closure that prints a list will borrow the list immutably, allowing you to continue using the list after the closure executes.
If a closure needs to modify a captured variable, it must capture by mutable reference. In this case, both the captured variable and the closure itself must be declared as mutable. The closure can then modify the captured variable, but the borrowing rules still apply – you cannot have other references to that variable while the mutable closure exists.
The most restrictive capture method is taking ownership, which moves the captured variables into the closure. This is necessary when the closure might outlive the scope where the variables were originally defined, such as when spawning threads. You can force ownership capture using the move keyword before the closure parameters: move |x| { /* closure body */ }. This is essential for thread safety, as threads cannot safely borrow from other threads that might terminate and drop their variables.

Closure Traits and Function Types

Rust represents closures through a trait system with three key traits: FnOnce, FnMut, and Fn. These traits form a hierarchy that describes how closures can be called and what they can do with captured variables.
FnOnce is the most basic trait that all closures implement. It represents closures that can be called at least once. Some closures, particularly those that move captured values or consume them in some way, can only be called once because they destroy or move their captured data during execution.
FnMut represents closures that can be called multiple times and may mutate their captured environment. These closures capture variables by mutable reference and can modify them across multiple calls. The borrowing rules ensure that when an FnMut closure is active, it has exclusive mutable access to its captured variables.
Fn is the most restrictive trait, representing closures that can be called multiple times without mutating their captured environment. These closures only capture by immutable reference and can be called concurrently without violating Rust's safety guarantees. If a closure implements Fn, it automatically implements FnMut and FnOnce as well, since being callable multiple times without mutation implies being callable with mutation and being callable once.

Working with Iterators

Iterators in Rust provide a way to process sequences of data. They are lazy, meaning they don't perform any work until you consume them by calling methods that actually iterate through the data. This lazy evaluation allows for efficient chaining of operations without creating intermediate collections.
The Iterator trait defines the core functionality with an associated type Item that represents what the iterator yields, and a next method that returns Option<Self::Item>. When next returns None, the iterator is exhausted. This design allows iterators to represent both finite and potentially infinite sequences safely.
You can create iterators from collections using methods like iter() for borrowing iteration, iter_mut() for mutable borrowing iteration, and into_iter() for consuming iteration. The choice between these methods depends on whether you need to modify elements and whether you want to consume the original collection.

Iterator Adaptors and Consumers

Iterator adaptors are methods that transform one iterator into another, allowing you to chain operations together. Common adaptors include map for transforming each element, filter for selecting elements based on a predicate, and enumerate for adding indices. These adaptors are lazy – they don't do any work until consumed.
The map method applies a closure to each element, transforming it into something else. For example, numbers.iter().map(|x| x * 2) creates an iterator that doubles each number. The filter method keeps only elements for which the predicate closure returns true: numbers.iter().filter(|&x| x > 10) keeps only numbers greater than ten.
Consumer methods actually iterate through the data and produce a final result. The collect method consumes an iterator and creates a collection from it. You often need to specify the collection type: let vec: Vec<_> = iterator.collect(). Other consumers include sum for adding numeric elements, fold for accumulating values with a custom operation, and for_each for executing side effects on each element.

Advanced Iterator Patterns

Additional iterator operations include zip for combining two iterators element-wise, chain for concatenating iterators, and filter_map for combining filtering and mapping in one operation. The zip method creates pairs from corresponding elements of two iterators: a.iter().zip(b.iter()) produces tuples (a[0], b[0]), (a[1], b[1]), ....
The fold method is useful for accumulating values. It takes an initial value and a closure that combines the accumulator with each element: numbers.iter().fold(0, |acc, x| acc + x) sums all numbers. This pattern can implement many other operations like finding maximum values, building strings, or creating complex data structures.
Iterator chains can express complex data transformations concisely. For example, processing audio data might involve: coefficients.iter().zip(buffer.iter()).map(|(c, b)| c * b).sum::<i32>() >> 12. This multiplies corresponding coefficients and buffer values, sums the results, and shifts the final value, all in a single readable expression.
fn main() { // Sample UTXOs: (txid_suffix, amount_sats) let utxos = vec![ ("a1b2", 50_000u64), ("c3d4", 15_000), ("e5f6", 100_000), ("g7h8", 3_000), ("i9j0", 75_000), ]; // Using closures and iterators to process UTXOs // 1. Filter UTXOs above dust threshold (10,000 sats) let spendable: Vec<_> = utxos .iter() .filter(|(_, amount)| *amount >= 10_000) .collect(); println!("Spendable UTXOs: {:?}", spendable); // 2. Calculate total balance with fold let total_balance: u64 = utxos .iter() .map(|(_, amount)| amount) .fold(0, |acc, amount| acc + amount); println!("Total balance: {} sats", total_balance); // 3. Find UTXOs needed to cover a 120,000 sat payment let target = 120_000u64; let mut accumulated = 0u64; let selected: Vec<_> = utxos .iter() .filter(|(_, amount)| *amount >= 10_000) // Skip dust .take_while(|(_, amount)| { if accumulated >= target { false } else { accumulated += amount; true } }) .collect(); println!("Selected for payment: {:?}", selected); // 4. Transform to display format using map and collect let display_strings: Vec<String> = utxos .iter() .map(|(txid, amount)| format!("{}...:{} sats", txid, amount)) .collect(); println!("Display: {:?}", display_strings); }

Introduction to Smart Pointers

Smart pointers are data structures that act like traditional pointers but provide additional capabilities and automatic memory management. Unlike simple references, smart pointers own the data they point to and can implement custom behavior for memory allocation, deallocation, and access patterns. They are essential tools for managing heap-allocated data and implementing complex ownership patterns that go beyond Rust's basic ownership system.
The "smart" aspect comes from their ability to automatically handle memory management tasks that would otherwise require manual intervention. When a smart pointer goes out of scope, it can automatically free associated memory, decrement reference counts, or perform other cleanup operations. This automation helps prevent memory leaks and use-after-free errors while providing more flexibility than stack-only allocation.
Smart pointers typically implement two key traits: Deref and Drop. The Deref trait allows the smart pointer to be used as if it were a reference to the contained data. The Drop trait enables custom cleanup logic when the smart pointer is destroyed. Together, these traits allow smart pointers to manage memory automatically.

The Box Smart Pointer

Box<T> is the simplest smart pointer, providing heap allocation for any type T. When you create a Box, the contained value is stored on the heap rather than the stack, and the Box itself (which is just a pointer) is stored on the stack. This indirection is useful when you need to store large amounts of data without moving it around, when you need a type with unknown compile-time size, or when you want to transfer ownership of heap data efficiently.
Creating a Box is straightforward: let boxed_value = Box::new(42); allocates an integer on the heap. The Box automatically manages this memory – when the Box goes out of scope, it automatically deallocates the heap memory. This automatic cleanup prevents memory leaks without requiring manual memory management.
One of the most important use cases for Box is enabling recursive data structures. Consider a linked list where each node contains a value and a pointer to the next node. Without Box, you cannot define such a structure because the compiler cannot determine the size of a type that contains itself. By using Box<Node> for the next pointer, you break the recursive sizing problem because Box has a known, fixed size regardless of what it contains.

Implementing the Deref Trait

The Deref trait allows a type to be dereferenced using the * operator, making smart pointers behave like references to their contained data. When you implement Deref for a smart pointer, you enable automatic dereferencing that makes the smart pointer transparent to use. This means you can call methods on the contained type directly through the smart pointer without explicit dereferencing.
The Deref trait defines an associated type Target that specifies what type of reference the dereference operation should produce. The trait requires implementing a deref method that returns a reference to the target type. For Box<T>, the implementation returns a reference to the contained T value.
Rust performs automatic deref coercion, which means the compiler can automatically insert calls to deref when needed to make types compatible. This is why you can pass a String to a function expecting a &str – the compiler automatically dereferences the String to get a string slice. This coercion can chain multiple levels, so a Box<String> can be automatically converted to a &str through multiple deref operations.

Custom Drop Implementation

The Drop trait allows you to specify custom cleanup code that runs when a value goes out of scope. This is particularly important for smart pointers that manage resources beyond simple memory, such as file handles, network connections, or reference counts. The Drop trait has a single method, drop, that takes a mutable reference to self and performs the cleanup.
Most types don't need custom Drop implementations because Rust automatically handles dropping their fields. However, smart pointers often need custom logic to properly clean up the resources they manage. For example, a reference-counted smart pointer needs to decrement the reference count and potentially deallocate shared data when the last reference is dropped.
You can also explicitly drop a value before it goes out of scope using std::mem::drop(). This function takes ownership of a value and immediately drops it, which can be useful for releasing resources early or ensuring cleanup happens at a specific point in your program. The explicit drop function is just an identity function that takes ownership – the real work happens when the value is dropped at the end of the function.
This foundation of closures, iterators, and smart pointers gives Rust developers tools for writing expressive, safe, and efficient code. These features work together to enable common programming patterns while maintaining Rust's core guarantees of memory safety and performance.
Quiz
Quiz1/5
What is the main benefit of Rust's functional programming features maintaining zero-cost abstractions?