Can I borrow the Moved Iterator?

ยท

4 min read

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 receiver self, which moves peekable

Back in the docs, we look for into_iter(). We learn that in the standard library, all Iterators 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!

Did you find this article valuable?

Support Charles Muchene by becoming a sponsor. Any amount is appreciated!