Custom `Iterator::try_fold` implementation

Let's say I want to implement custom try_fold for my iterator. Docs say:

If multiple calls are needed, the ? operator may be convenient for chaining the accumulator value along, but beware any invariants that need to be upheld before those early returns.

I'm not sure what invariants they mean.
Do they try to warn me of situation like this?

(Hypothetical custom implementation)

fn try_fold<B, F, R>(&mut self, init: B, mut f: F) -> R
    where
        Self: Sized,
        F: FnMut(B, Self::Item) -> R,
        R: Try<Output = B>,
{
    let mut accum = init;
    let mut container = Vec::new();
    // let's pretend the implementation needs this
    while let Some(x) = self.next() {
        container.push(x);
    } 
    // after this, iterator is empty
    for item in container {
        accum = f(accum, item)? 
        // when this early returns (before processing all items from container)
        // iterator will empty
    }
    try { accum }
}

That's a weird implementation of try_fold. Usually try_fold is assumed to behave identical to the default implementation, except perhaps for performance which can sometimes be significantly better if try_fold is implemented directly.

The default implementation only consumes items up to the point an "error" value is produced (i. e. something that makes branch return a ControlFlow::Break value).

(edited, thanks @H2CO3)

1 Like

I assume you meant ControlFlow::Break there.

1 Like

Yes, but that isn't the point.
The point is I want to know what do the docs mean "upholding invariants in try_fold implementation". Let me quote (source - Note to Implementors):

If multiple calls are needed, the ? operator may be convenient for chaining the accumulator value along, but beware any invariants that need to be upheld before those early returns.

I thought my intentionally weird implementation doesn't uphold "invariants", because it doesn't allow to use the iterator after something going wrong when accumulating. If that isn't the case - can you provide me some example when "invariants" aren't upheld in try_fold implementation?

Whatever invariants your iterator needs, so that, for example, calling next after the try_fold has exited before exhausting the iterator will resume iteration in the correct place.

For example, if you were implementing Take::try_fold, you's need to ensure that the "number of elements left to return" field is updated appropriately.

We had a bunch of PRs back in the day to customize try_fold on adapters that didn't uphold this requirement, thus the note in the docs about it.

2 Likes

Thank you for provided example!

So, let's say MyIterator consists of internal_iter and items_left field and when I implement try_fold on MyIterator I should avoid calls like self.internal_iter.try_fold(args)?, because when it early returns I don't update items_left field (which, for example, naive implementation would update only after successful internal_iter.tryfold call).

Is this what "Note to Implementors" try to warn me about?

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.