Traits and Iteration
Shared behavior, loops, and parallelism
Recap
So far we have seen
- Rust’s basic types,
struct,enum, generics - Ownership, borrowing, lifetimes
Next: how to define shared behavior across types and use it to iterate over data
- Traits: shared behavior (cf. Haskell’s typeclasses)
- Iterators:
forloops,map,filter,collect - Parallel Iteration: with
rayon
Today’s Goals
- Traits: shared behavior (cf. Haskell’s typeclasses)
- Iterators:
forloops,map,filter,collect - Parallel Iteration: with
rayon
What is a Trait?
A trait defines a set of methods that a type must implement …
… aka typeclass in Haskell!
Haskell
class Summary a where
summarize :: a -> StringRust
trait Summary {
fn summarize(&self) -> String;
}(Like typeclasses) traits let us write code that works with any type that implements that interface.
Implementing a Trait
Haskell
data Article = MkArticle
{ headline :: String
, author :: String
}
instance Summary Article where
summarize a = headline a ++ ", by " ++ author aRust
struct Article {
headline: String,
author: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}, by {}", self.headline, self.author)
}
}
Multiple Types, One Trait
Different types can implement the same trait with different behavior
struct Post {
username: String,
content: String,
}
impl Summary for Post {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}Now we can call summarize() on both Article and Post
fn main() {
let a = Article { headline: String::from("Penguins Win!"),
author: String::from("Iceburgh") };
let p = Post { username: String::from("horse_ebooks"),
content: String::from("of course as you know") };
println!("{}", a.summarize()); // "Penguins Win!, by Iceburgh"
println!("{}", p.summarize()); // "horse_ebooks: of course as you know"
}
Traits as Parameters
We can write functions that accept any type implementing a trait
Haskell: “Constraint”
notify :: (Summary a) => a -> String
notify item = "Breaking news! " ++ summarize item- Haskell calls
Summary aa constraint on the type variablea
Rust: “Trait Bound”
fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}- Rust calls
T: Summarya trait bound on the type variableT
Multiple Trait Bounds
A parameter can require multiple traits with +
fn notify<T: Summary + std::fmt::Display>(item: &T) {
println!("Breaking news! {}", item.summarize());
}When bounds get long, use a where clause:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Summary + Clone,
U: Clone + std::fmt::Debug,
{
todo!()
}
QUIZ
trait Area {
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Rect { width: f64, height: f64 }
impl Area for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
fn print_area<T: Area>(shape: &T) {
println!("Area: {}", shape.area());
}
fn main() {
let c = Circle { radius: 10.0 };
let r = Rect { width: 3.0, height: 4.0 };
print_area(&c);
print_area(&r);
}What happens?
Compiler Error: Trait Not Implemented
error[E0277]: the trait bound `Rect: Area` is not satisfied
--> src/main.rs:20:17
|
20 | print_area(&r);
| ---------- ^^ the trait `Area` is not implemented for `Rect`
| |
| required by a bound introduced by this callFix: implement Area for Rect!
impl Area for Rect {
fn area(&self) -> f64 {
self.width * self.height
}
}
Familiar Traits: Debug, Clone, Copy
We’ve already used traits via #[derive(...)]!
#[derive(Debug, Clone, Copy)]
struct Circle {
x: f64,
y: f64,
radius: f64,
}Debuglets you print with{:?}Clonegives you.clone()Copymakes assignment copy instead of move
These are all traits defined in the standard library.
Goal Today
Traits: shared behavior (cf. Haskell’s typeclasses)- Iterators:
forloops,map,filter,collect - Parallel Iteration: with
rayon
The Iterator Trait
Iteration in Rust is built on a simple trait
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}type Itemis the type of elements produced (an associated type)next()returnsSome(value)orNonewhen done- Calling
next()consumes one element (hence&mut self)
Calling next Directly
fn main() {
let v = vec![10, 20, 30];
let mut iter = v.iter();
println!("{:?}", iter.next()); // Some(10)
println!("{:?}", iter.next()); // Some(20)
println!("{:?}", iter.next()); // Some(30)
println!("{:?}", iter.next()); // None
}Note: iter must be mut because next() advances internal state.
for Loops Use Iterators
A for loop is syntactic sugar for calling next() repeatedly
fn main() {
let v = vec![10, 20, 30];
for val in v.iter() {
println!("Got: {val}");
}
}Prints:
Got: 10
Got: 20
Got: 30The for loop calls next() behind the scenes until it gets None.
Three Ways to Iterate
| Method | Produces | Ownership |
|---|---|---|
.iter() |
&T |
shared borrow |
.iter_mut() |
&mut T |
mutable borrow |
.into_iter() |
T |
takes ownership |
QUIZ
fn main() {
let v = vec![String::from("a"), String::from("b")];
for s in v.iter() {
println!("{s}");
}
println!("{:?}", v);
}fn main() {
let mut v = vec![String::from("a"), String::from("b")];
for s in v.iter_mut() {
s.push_str("_foo");
}
println!("{:?}", v);
}fn main() {
let v = vec![String::from("a"), String::from("b")];
for s in v.into_iter() {
println!("{s}");
}
println!("{:?}", v);
}
Iterator Adaptors: map
Iterator adaptors transform one iterator into another.
Haskell
map (+1) [1, 2, 3] -- [2, 3, 4]Rust
fn main() {
let v = vec![1, 2, 3];
let v2: Vec<i32> = v.iter().map(|x| x + 1).collect();
println!("{:?}", v2); // [2, 3, 4]
}|x| x + 1is a closure (like Haskell’s\x -> x + 1).collect()consumes the iterator and builds aVec
Important: iterators are lazy – nothing happens until you consume them!
Iterator Adaptors: filter
Haskell
filter even [1, 2, 3, 4, 5] -- [2, 4]Rust
fn main() {
let v = vec![1, 2, 3, 4, 5];
let evens: Vec<&i32> = v.iter().filter(|x| *x % 2 == 0).collect();
println!("{:?}", evens); // [2, 4]
}filter keeps elements where the closure returns true.
Chaining Adaptors
You can chain map, filter, and other adaptors together
Haskell
sum (map (*2) (filter even [1..10])) -- 60Rust
fn main() {
let v: Vec<i32> = (1..=10).collect();
let result: i32 = v.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * 2)
.sum();
println!("{result}"); // 60
}Read it as a pipeline: start with 1..=10, keep evens, double them, sum.
QUIZ
What does this print?
fn main() {
let words = vec!["hello", "world", "hi"];
let result: Vec<_> = words.iter()
.filter(|w| w.len() > 2)
.collect();
println!("{:?}", result);
}
Answer
["hello", "world"]filterkeeps words with length > 2:"hello","world"copiedconverts&&strto&strcollectbuilds aVec<&str>
Goal Today
Traits: shared behavior (cf. Haskell’s typeclasses)Iterators:forloops,map,filter,collect- Parallel Iteration: with
rayon
Parallel Iteration with Rayon
What if you want to speed up iteration by using multiple cores?
In most languages, parallelism is hard and error-prone (data races, deadlocks, …).
Rust’s ownership it safe; Rayon makes it easy.
Sequential vs Parallel: One Line Change
Sequential
let total: i32 = data.iter()
.filter(|x| is_interesting(x))
.map(|x| heavy_computation(x))
.sum();Parallel (with Rayon)
use rayon::prelude::*;
let total: i32 = data.par_iter()
.filter(|x| is_interesting(x))
.map(|x| heavy_computation(x))
.sum();Just change .iter() to .par_iter() … that’s it!
Rayon automatically splits the work across threads.
Rayon’s Parallel Iterators
Three parallel equivalents of the sequential methods:
| Sequential | Parallel | Ownership |
|---|---|---|
.iter() |
.par_iter() |
shared borrow |
.iter_mut() |
.par_iter_mut() |
mutable borrow |
.into_iter() |
.into_par_iter() |
takes ownership |
All the familiar adaptors work: map, filter, sum, collect, …
Example: Parallel Sum of Squares
use rayon::prelude::*;
fn sum_of_squares(v: &[i64]) -> i64 {
v.par_iter()
.map(|x| x * x)
.sum()
}
fn main() {
let data: Vec<i64> = (1..=1_000_000).collect();
let result = sum_of_squares(&data);
println!("Sum of squares: {result}");
}Rayon handles thread creation, work splitting, and joining – all behind the scenes.
Why is Rayon Safe?
Why can’t we have data races?
Sequential: This compiles
let mut total = 0;
data.iter()
.filter(|x| is_interesting(x))
.for_each(|x| total += heavy_computation(x));Parallel: This does NOT compile!
let mut total = 0;
data.par_iter()
.filter(|x| is_interesting(x))
.for_each(|x| total += heavy_computation(x)); // ERROR!error: cannot assign to `total`, as it is a captured variable
in a `Fn` closure
Rust’s Type System Prevents Data Races
Rayon requires closures to be Fn (not FnMut)
Fn: can only read captured variables (shared access)FnMut: can modify captured variables (exclusive access)
Multiple threads running total += ... would be a data race!
The compiler rejects this at compile time.
Fix: use Rayon’s built-in sum(), which safely combines partial results
let total: i32 = data.par_iter()
.filter(|x| is_interesting(x))
.map(|x| heavy_computation(x))
.sum(); // safe parallel reductionThis is Rust’s fearless concurrency: the compiler catches threading bugs before your code runs.
QUIZ
Which of these can be safely parallelized with par_iter?
// (A) sum the lengths of strings
let total: usize = words.iter()
.map(|w| w.len())
.sum();// (B) collect uppercase versions
let upper: Vec<String> = words.iter()
.map(|w| w.to_uppercase())
.collect();// (C) push into a shared vec
let mut results = Vec::new();
words.iter()
.for_each(|w| results.push(w.to_uppercase()));
Answer
- (A) Yes!
map+sumis safe to parallelize; no shared mutable state. - (B) Yes!
map+collectis safe; each closure produces an independent value. - (C) No!
results.push(...)mutates a sharedVec– the compiler would reject the parallel version.
The pattern: if your pipeline uses pure functions (no side effects), it parallelizes safely.
Using Rayon: Setup
Add rayon to your Cargo.toml:
[dependencies]
rayon = "1.10"Then import the prelude:
use rayon::prelude::*;That’s it! Now .par_iter(), .par_iter_mut(), and .into_par_iter() are available on standard collections.
Goal Today
Traits: shared behavior viatrait,impl Trait for Type, trait boundsIterators:Iteratortrait,forloops,map,filter,collectParallel Iteration: Rayon’spar_iter, fearless concurrency
Summary
Traits define shared behavior across types
- Like Haskell’s typeclasses
- Use
impl TraitorT: Traitfor generic functions
Iterators provide a uniform way to process sequences
forloops,map,filter,collect,sum, …- Lazy: nothing happens until consumed
Rayon makes parallelism a one-line change
- Replace
.iter()with.par_iter() - Rust’s type system prevents data races at compile time
- “Fearless concurrency”
Material Inspired by
- https://doc.rust-lang.org/book/ch10-02-traits.html
- https://doc.rust-lang.org/book/ch13-02-iterators.html
- https://developers.redhat.com/blog/2021/04/30/how-rust-makes-rayons-data-parallelism-magical