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\$ Commented Dec 13, 2022 at 7:07

You must log in to answer this question.