Table of contents
The concept of Ownership is no doubt one of Rust's most unique feature. I recently worked on a command line program using Rust and encountered a scenario where I couldn't get Ownership right. In this article, we discuss a similar scenario and my solution.
Scenario
Say we are required to print all the numbers in a given range. But before printing the last number, output "I knew it was <last-number>!".
fn print_em(numbers: Range<u32>) {
let last = numbers.end;
for number in numbers {
if number == last {
println!("I knew last was {}!", number);
}
println!("{number}");
}
}
Straightforward! The requirements are changed such that we're given an Iterator
instead of a Range
. A possible refactor would look like the following:
fn print_em(numbers: impl Iterator<Item=u32>) {
let last = numbers.last().expect("No last number");
for number in numbers {
if number == last {
println!("I knew last was {}!", number);
}
println!("{number}");
}
}
In this snippet, we change the type of the parameter to an impl Iterator<Item=u32>
i.e. any type that implements the Iterator
trait with an associated type of u32
.
Our program, however, fails to compile after this change. To determine the last value, we call numbers.last()
. This call moves numbers
because we have to consume the iterator to reach the last number. numbers
is then invalidated and we cannot use it again. This move is highlighted by the compiler as shown below:
Is there another way to determine the last item in an iterator without consuming it?
A Peekable approach
Checking the docs, we find Peekable
- an iterator that wraps another iterator. Peekable
provides a peek()
method that will return an optional reference to the next()
value without advancing the iterator. Hmm. Promising.
Using a Peekable
while inside an iteration, we can peek()
and inspect the optional. If it is the None
variant, then we are in the last iteration. Perfect!
fn print_em(numbers: impl Iterator<Item=u32>) {
let mut peekable = numbers.peekable();
for number in peekable {
if let None = peekable.peek() {
println!("I knew last was {}!", number);
}
println!("{number}");
}
}
Refactor. Save. cargo check
. Bam! 🤯
What now? Another move? Well, I suppose these are common gotchas in Rust. Let's take a closer look at the note in the compiler output:
into_iter
takes ownership of the receiverself
, which movespeekable
Back in the docs, we look for into_iter()
. We learn that in the standard library, all Iterator
s implement the IntoIterator
trait and just return themselves. Also, when using a for loop, some de-sugaring happens at compile time for implementations of IntoIterator
i.e. peekable::into_iter()
is called and that moves our peekable
!
Recap: we can take a peek into the future but our iterator is consumed by the de-sugar magic. We, therefore, need to get around this move or avoid it altogether. So, what's our next move? 😎
Change the loop structure
Rust has three kinds of loops:
for
- loop over an iterator -- not suitable for our situation 🙅🏾♂️loop
- loop forever until we stop it -- a possible solution 🤷🏾♂️while
- loop while a condition is true -- a better solution 🙋🏾♂️
Using a while
loop syntax, we can pattern-match the peekable.next()
and bind the value to number
in each iteration. With this change, we can iterate over our numbers
and peek()
to determine if the current number is the last one. Mission accomplished!
fn print_em(numbers: impl Iterator<Item=u32>) {
let mut peekable = numbers.peekable();
while let Some(number) = peekable.next() {
if let None = peekable.peek() {
println!("I knew last was {}!", number);
}
println!("{number}");
}
}
The code above is safe, consumes the iterator once and adapts to an iterator of any given number of elements. Bam! 🥳
Conclusion
Rust, like any language, takes some getting used to. Learning the gotchas, the standard library and, generally, getting a hang of things in an ecosystem. What better way to learn anything than by doing it?
Tip: when working on a project, have a browser tab open at https://doc.rust-lang.org/std/index.html. Searching works like a charm and the docs are even more charming!
Happy coding!