0
\$\begingroup\$

I want to extend generic iterators with a convenience function until() that works like an inversion of take_while(). Here is the code:

Cargo.toml:

[package]
name = "until"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

src/lib.rs:

use std::iter::TakeWhile;

pub trait Until<T, P>: Iterator<Item = T> + Sized
where
    P: Fn(&Self::Item) -> bool + 'static,
{
    fn until(&mut self, f: P) -> TakeWhile<&mut Self, Box<dyn Fn(&T) -> bool>> {
        self.take_while(Box::new(move |item| !f(item)))
    }
}

impl<T, P, I> Until<T, P> for I
where
    P: Fn(&Self::Item) -> bool + 'static,
    I: Iterator<Item = T>,
{
}

src/main.rs:

use until::Until;

fn main() {
    let items = vec![1, 2, 3, 4, 5];

    for item in items.into_iter().until(|item| *item > 3) {
        println!("{}", item);
    }
}

What I don't like about this implementation are

  1. The use of a Box to wrap the function pointer
  2. The thus resulting complex return type of until()
  3. The fact that I explicitly need to implement the trait for all iterators in an empty impl block

How can I address these three issues (and possibly others)?

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

My first thought: why? If all you are going to do is invert the logic of take_while, why don't I just call take_while with an extra !

  1. The use of a Box to wrap the function pointer
  2. The thus resulting complex return type of until()

Typically, what's done is that the iterator is implemented as a struct generic over the internal iterator type. So you'd implement a:

struct TakeUntil<I, P> {iterator: I, predicate: P);

impl <I:Iterator, P: FnMut(&I::Item) -> bool> Iterator for TakeUntil<I> {
   ...
}

Then your until function would return this type:

fn until(&mut self, f: P) -> TakeUntil<Self, P>

That makes the return type simpler and avoid allocation and dynamic dispatch, but does require additional ceremony.

However, its also odd here because iterators typically take self not &mut self. Put another way, they consume the iterator they don't modify it.

  1. The fact that I explicitly need to implement the trait for all iterators in an empty impl block

I don't believe you can avoid this. What you can do is move the implementation to the impl block and remove much of what is currently on the trait:

pub trait Until<T, P>
{
    fn until(&mut self, f: P) -> TakeWhile<&mut Self, Box<dyn Fn(&T) -> bool>>;
}

impl<T, P, I> Until<T, P> for I
where
    P: Fn(&I::Item) -> bool + 'static,
    I: Iterator<Item = T>,
{
    fn until(&mut self, f: P) -> TakeWhile<&mut Self, Box<dyn Fn(&T) -> bool>> {
        self.take_while(Box::new(move |item| !f(item)))
    }
}

This is what I typically see for iterator extensions.

\$\endgroup\$
1
  • \$\begingroup\$ As for the why: It's just syntactic sugar. Oftentimes I want to filter an iterator until a condition is met. It's less cognitive load than always having to invert the condition and use take_while() instead. \$\endgroup\$ Dec 13, 2022 at 7:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.