The anti-iterator pattern, does it exist?

I'm also reminded of the various channel implementations, including channel in std::sync::mpsc - Rust

They are formed of a matched sender and receiver, where the receiver is an Iterator in spirit, if not actually, and the sender is your "anti iterator" equivalent. I'm not aware of any synchronous version using a trait to abstract the interface though.

3 Likes

Sorry, could you elaborate on this? I also thought about try_for_each(), but I can't see what you mean by being unable to zip two iterators in this context.

I mean this:

use std::fmt::Debug;

fn foo<T, E, A>(a: A) -> Result<(), E>
where
    T: Debug,
    A: IntoIterator<Item = Result<T, E>>,
{
    a.into_iter().try_for_each(|x| {
        let x = x?;
        println!("a => {x:?}");
        Ok(())
    })
}

fn bar<T, E, A, B>(a: A, b: B) -> Result<(), E>
where
    T: Debug,
    A: IntoIterator<Item = Result<T, E>>,
    B: IntoIterator<Item = Result<T, E>>,
{
    let mut a = a.into_iter();
    let mut b = b.into_iter();
    // How to write this with `try_for_each`:
    loop {
        match a.next() {
            Some(Ok(x)) => println!("a => {x:?}"),
            Some(Err(e)) => return Err(e),
            None => break,
        }
        match b.next() {
            Some(Ok(x)) => println!("b => {x:?}"),
            Some(Err(e)) => return Err(e),
            None => break,
        }
    }
    Ok(())
}

(Playground)

I can write foo with try_for_each, but how to do this in the bar function?


You could use Iterator::zip in this case, I guess (Playground). But how about other cases like:

-        match a.next() {
-            Some(Ok(x)) => println!("a => {x:?}"),
-            Some(Err(e)) => return Err(e),
-            None => break,
-        }
+        for _ in 0..2 {
+            match a.next() {
+                Some(Ok(x)) => println!("a => {x:?}"),
+                Some(Err(e)) => return Err(e),
+                None => break,
+            }
+        }
         match b.next() {
             Some(Ok(x)) => println!("b => {x:?}"),
             Some(Err(e)) => return Err(e),
             None => break,
         }

(Playground)

Wouldn't such a behaviour be achievable through Generator functions?

That zip version isn't quite correct: if b returns None, the original version will have already output the last a value, whereas the zip version will discard the a value as soon as b ends. To get the correct output, we have to do something wonkier:

use std::fmt::Debug;

fn bar<T, E, A, B>(a: A, b: B) -> Result<(), E>
where
    T: Debug,
    A: IntoIterator<Item = Result<T, E>>,
    B: IntoIterator<Item = Result<T, E>>,
{
    let a = a.into_iter();
    let mut b = b.into_iter();
    a.map(|x| (x, b.next()))
        .try_for_each(|(x, y)| {
            let x = x.map_err(Err)?;
            println!("a => {x:?}");
            let y = y.ok_or(Ok(()))?.map_err(Err)?;
            println!("b => {y:?}");
            Ok(())
        })
        .map_or_else(|result| result, |_| Ok(()))
}

But that still isn't entirely equivalent in behavior, since it will pull an extra element from b even after a returns Some(Err). We could instead use Itertools::interleave_shortest():

use itertools::Itertools;
use std::fmt::Debug;

fn bar<T, E, A, B>(a: A, b: B) -> Result<(), E>
where
    T: Debug,
    A: IntoIterator<Item = Result<T, E>>,
    B: IntoIterator<Item = Result<T, E>>,
{
    let a = a.into_iter().map(|x| ("a", x));
    let b = b.into_iter().map(|x| ("b", x));
    a.interleave_shortest(b).try_for_each(|(src, x)| {
        let x = x?;
        println!("{src} => {x:?}");
        Ok(())
    })
}

I'll admit that this does get even more unwieldy with your last example. What I'd do to generalize this is to create a small state machine to pull from the iterators in the correct sequence:

use std::{fmt::Debug, iter};

fn bar<T, E, A, B>(a: A, b: B) -> Result<(), E>
where
    T: Debug,
    A: IntoIterator<Item = Result<T, E>>,
    B: IntoIterator<Item = Result<T, E>>,
{
    let mut a = a.into_iter();
    let mut b = b.into_iter();
    let mut i = 0;
    iter::from_fn(|| {
        if i < 2 {
            i += 1;
            a.next().map(|x| ("a", x))
        } else {
            i = 0;
            b.next().map(|x| ("b", x))
        }
    })
    .try_for_each(|(src, x)| {
        let x = x?;
        println!("{src} => {x:?}");
        Ok(())
    })
}

I concur with @J-Cake, that generators would be very useful here on the pulling end.

Anyway, after doing this exercise, I think that the main limitation of try_for_each() here is that there's only one cancellation point after each element: you can't partially pull a value from the iterator. This is mainly relevant when you're pulling from one iterator, and only conditionally pulling from another iterator. Of course, the pulling logic could also be split into a bunch of map()s or map_while()s before the final try_for_each().

I think it's similar to cpp's OutputIterator.
Example: std::back_inserter create a "anti-iterator" that "push_back" a new value to the underlying container every time.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.