Python has this really neat feature called list comprehensions.
# A list of bands
=
# A list comprehension to filter for bands that start with "M"
=
# A list comprehension to uppercase the bands
=
# We get ["METALLICA", "MEGADETH"]
List comprehensions are a concise way to create lists from other lists, but they work with any iterable, like dicts!
=
=
# tolkien_books = ["The Lord of the Rings", "The Hobbit"]
As a Pythonista, list comprehensions became second nature to me. Their elegance is hard to beat. For a long time, I wished Rust had something similar. It was one of the few things I profoundly missed from Python — until I learned more about the philosophy behind Rust’s iterator patterns.
Enter Rust’s Iterators
Rust has a notion of chainable operations on iterators, forming a pipeline where
each operation is applied to every element in sequence. Two of the most common
operations are map
, which applies a function to each item, and filter
, which
selectively includes items that meet a certain condition.
Here’s the equivalent Rust code for the first Python example above:
let bands = vec!;
let uppercased: = bands.iter
.filter
.map
.collect;
// uppercased = vec!["METALLICA", "MEGADETH"]
Sure, it’s a tad more verbose than Python’s list comprehensions, but oh, the versatility!
For example, debugging an iterator chain is as simple as inserting an inspect
wherever you want to peek at the elements:
let uppercased: = bands
.iter
.filter
.inspect
.map
.collect;
Achieving the same effect with list comprehensions in Python is quite tricky.
I often find myself chaining iterator operations in Rust. And honestly? It’s pretty intuitive. Over time, I’ve even grown fond of its explicitness. (Or, you know, maybe it’s just some form of Stockholm syndrome.)
What I like the most is the flexibility of this pattern. Let me demonstrate!
Collecting into different types
In Python, you can collect into different types like a set
:
=
# It's a set!
# tolkien_books = {"The Lord of the Rings", "The Hobbit"}
Note that the notation is irritatingly different from a list comprehension. Instead of square brackets, we suddenly use curly braces now.
In contrast, to collect into different types in Rust, just specify the type you want to collect into; easy as cake!
let books = from_iter;
// Collect into a vector
let tolkien_books: = books
.iter
// Look at the second element of the tuple, which is the author
.filter
// Now only take the first element of the tuple, which is the book
.map
// Collect into a vector
.collect;
// Alternatively, collect into a set.
// This works because `collect` can collect into any type that implements
// `FromIterator`, which `HashSet` does.
let tolkien_books: = books
.iter
.filter
.map
.collect;
What if we wanted to count the number of bands that start with the same letter?
let first_letters: = bands
.iter
// Filter out bands that don't have a first letter.
// (Yes, that's possible because we accept any string as input.)
// `filter_map` is like `map` but it filters out `None` values.
// If the band is empty, `chars().next()` will return `None`.
.filter_map
// Start counting the occurrences of each letter
.fold;
// Printing the result
for in &first_letters
Which gives us:
M: 2
I: 1
A: 1
J: 1
Neat!
In Python, you would probably stop using list comprehensions altogether and
use the builtin Counter
for this:
# Note that we handle the case where a band
# is an empty string with the `if len(band)` condition
=
That’s another API to learn and remember.
Rust also has a Counter
implementation, but it lives outside the standard
library in the counter crate.
Nevertheless, it fits right in – like a natural extension of the standard library.
use Counter;
let first_letters: = bands
.iter
.filter_map
.collect;
We still use the same patterns that we used before and we didn’t have to refactor our code. It was all very seamless. Again, we just changed the type we collect into!
Such a deep integration into the the iterator API would be much harder, impossible even, in Python.
In Rust you get the best of both worlds: the flexibility of the ecosystem and the native feel of the standard library.
Behind the Scenes of collect
The convenience of collecting into a Counter
is made possible by the fact that
Counter
implements FromIterator
.
That’s all the compiler needs to know to be able to use collect
with Counter
.
Let’s peek behind the curtain and see how it works.
Here is a code snippet from the counter
crate:
You can see that it just calls Counter::init
where init
is defined as:
That might look a little intimidating at first, but if you squint, you’ll see that Counter
uses a for
loop to iterate over the elements of the iterator and also uses the entry
API to insert a new key or increment the value; just like we did manually before.
The final, missing piece can be found in the Rust standard library
in the Iterator
trait:
As you can see, the collect
method is implemented for all types that implement
FromIterator
and it’s just a thin wrapper around FromIterator::from_iter
.
The Counter
crate implements FromIterator
and therefore we can use
it in combination with collect
.
It’s simple and effective — and without knowing all the details, it can
feel like magic.
In reality though, the flexibility is made possible by the trait system and the
iterator API.
If you want to learn more, the counter
source
code makes for
an interesting read.
What’s important is that this integration needs to be done only once and then it
can be used by everyone. So as a mere user, you don’t need to know how collect
works, just that it does.
Conclusion
It may be just a personal anecdote, but I feel like more experienced Rust
developers tend to prefer iterators over other methods of iteration like
for
loops.
One reason might be that iterators are more versatile because they can be chained and collected into custom types as we have seen and they scale well with the complexity of the problem: at no point are you forced to use a different API or pattern.
Another reason why I like iterator chains is that naming things is hard. With iterators, you don’t need to come up with a name for the intermediate steps.
Just do me a favor and don’t create pipelines which are super hard to read. If the iterator chain is too long, or a step in the chain is longer than a few lines, there’s no shame in breaking it up into multiple steps.
My hope is that I was able to show you how powerful iterator patterns in Rust are and they are a powerful stand-in for those list comprehensions you might know and love from Python.
I encourage you to explore the iterator API in Rust and see how you can use it to make your code more expressive and concise.