itertools icon indicating copy to clipboard operation
itertools copied to clipboard

Implement peeking_fold_while

Open mingyli opened this issue 5 years ago • 5 comments

Issue

try_fold is capable of folding an iterator until a short-circuiting condition is met. For instance, the short-circuiting example in the docs sums up integers until an integer overflow is encountered. However, try_fold ends up consuming the element that caused the short-circuit (in this case, the element 100).

let a = [10, 20, 30, 100, 40, 50];
let mut it = a.iter();
let sum = it.try_fold(0i8, |acc, &x| acc.checked_add(x).ok_or(acc));
assert_eq!(sum, Err(60));
assert_eq!(it.next(), Some(&40));

Solution

I've implemented a PeekingFoldWhile trait for Peekables and PutBacks with a method peeking_fold_while that does not consume the short-circuiting element. Using it on the above example leads to the following behavior:

use itertools::Itertools;

let mut it = a.iter().peekable();
let sum = it.peeking_fold_while(0i8, |acc, &&x| acc.checked_add(x).ok_or(acc));
assert_eq!(sum, Err(60));
assert_eq!(it.next(), Some(&100));

This is my first attempt at contributing so feedback is welcome 😄

mingyli avatar Jan 26 '20 08:01 mingyli

This is an interesting idea, and maybe I am missing something, but I suspected that try_fold allows keeping the failure element by specifying an appropriate accumulation function:

let a = [10, 20, 30, 100, 40, 50];
let mut it = a.iter();
let sum = it.try_fold(0i8, |acc, &x| acc.checked_add(x).ok_or((acc, x)));
assert_eq!(sum, Err((60, 100)));

This has the advantage that it works for any Iterator and it is possibly more efficient (Peekable.next must check if there currently is a peeked-element, right).

Could you shed some light on the advantages of peeking_fold_while over the solution via try_fold?

phimuemue avatar Jan 30 '20 21:01 phimuemue

I actually hadn't thought of your solution and its existence probably eliminates a lot of use cases of peeking_fold_while. I suppose one benefit is that peeking_fold_while enables folding the same iterator multiple times without dropping elements, by not shifting the responsibility of holding onto the short-circuit element to the user. For example,

//       <---->  <----->  <---->
let a = [20, 30, 100, 10, 40, 50];
let mut it = a.iter().peekable();
let sum = it.peeking_fold_while(0i8, |acc, &&x| acc.checked_add(x).ok_or(acc));
assert_eq!(sum, Err(50));
let sum = it.peeking_fold_while(0i8, |acc, &&x| acc.checked_add(x).ok_or(acc));
assert_eq!(sum, Err(110));
let sum = it.peeking_fold_while(0i8, |acc, &&x| acc.checked_add(x).ok_or(acc));
assert_eq!(sum, Ok(90));

whereas using try_fold like above would require unpacking the results to obtain the values 100 and 40, and then passing those into the second and third fold calls. I guess we'd have to determine whether the additional benefit of this interface outweighs polluting itertools with something that is slightly different from something already in the standard library.

mingyli avatar Feb 01 '20 08:02 mingyli

Thank you for your elaboration. I agree, we should see if it's worth to include it in the crate.

As a sidenote: The use case you gave reminded me of something like group_while, which I could not find anywhere... Does anyone know if a functionality like this exists in Iter or Itertools?

phimuemue avatar Feb 01 '20 15:02 phimuemue

Given that there is precedence with peeking_take_while is this something we should revisit?

mingyli avatar Jan 01 '21 02:01 mingyli