- Feature Name: exhaust_control_nr2_fuck_the_planet - Start Date: - RFC PR: (leave this empty) - Rust Issue: (leave this empty) # Summary [summary]: #summary Add an adapter `.exhausting()` to `Iterator` which causes the iterator to be driven to its end on drop. Before dropping it will act exactly like the source iterator. # Motivation [motivation]: #motivation This RFC is part of two RFCs for splitting up the functionality of `drain()` into orthogonal APIs. It's therefore related to [the RFC for non-selfexhausting drains](https://hackmd.io/s/rksDventG#), but not dependent on it. The current `drain` APIs run the following code on drop before repairing the collection's state: ```rust for _ in &mut self {} ``` That is, they run themselves to the end for the side-effect of calling the destructor on each element to be removed from the collection. This showcases the use of self-exhausting iterators. The principle can apply to any side effecting iterator where all side effects are needed but not (all) the elements it returns. An `.exhausting()` adapter allows adding self-exhaustion on drop to arbitrary iterators. In the case of `drain`, the same behaviour could be attained by combining two separate functions: `drain_nonexhausting().exhausting()` Iteration through `.by_ref()` and subsequent consumption can achieve the same result, but only when one is holding the iter. With `.exhausting()`, the iter can be passed to a function or returned from one. It also has better compatibility with method chaining. Note that returning a self-exhausting iterator from a function should mostly be limited to callback situations. Hardcoding self-exhaustion is mixing concerns and needlessly limiting. Adding `exhausting` to the std library should make it easy for users to gain this behaviour where necessary and avert more non-lazy iterator APIs in the std library and outside of it. ## Examples ```rust // pass side effecting iter away iter_of_iters.flat_map(|iter| { iter.map(side_effects) .exhausting() // finish what you've started .take_while(condition) }); // return self-exhausting iter from function fn drain(&mut self) -> Drain { // wrapper can forward all iterator methods to internal iter Drain( self.drain_nonexhausting() .exhausting() ) } // -------------------- Aesthetic improvements only --------------------- // Current: manual exhaustion with by-ref let mut iter = iter.some() .adapter() .chain(); let val = iter.by_ref() // chain breaking indirection .map(func) find(condition); iter.for_each(|_| {}); // explicitly consume iterator // must have access to iter // With proposal: let val = iter.some() // all of this .adapter() // will run .chain() // for all elements in iter .exhausting() .map(func) // runs only until an element .find(condition); // is found or iter is exhausted //---------------------------------------------------------------------- ``` # Implementation During iteration, the `Exhausting` adapter is a trivial wrapper that acts like `&mut Self`, meaning it implements all the Iterator traits that the contained iter implements and will always do external iteration. On drop, it runs `for _ in self {}`. # Guide-level explanation [guide-level-explanation]: #guide-level-explanation The iterator created by `iter.exhausting()` behaves in the same fashion as `iter` and will on drop be automatically driven to its end. This is useful, if `iter` causes side-effects and is passed away so that one can't exhaust it explicitly with `.for_each(drop)`. # Drawbacks [drawbacks]: #drawbacks * `.exhausting()` may be too niche a usecase with `drain` already forcing the behaviour. * The `Exhausting` adapter has a corner case on finished, non-fused iterators. On drop, it will attempt iteration again which will result in implementation dependent behaviour unless guarded against with a flag and a comparison on every `.next()`. # Rationale and alternatives [alternatives]: #alternatives * Add the adapter to itertools. * Add `exhausting` to the `FusedIterator` trait to side-step the issue of non-fused iterators altogether. # Unresolved questions [unresolved]: #unresolved-questions * Self-exhausting iterators that can panic during `next()` can easily run into double panics. If `next()` panics during normal program execution, then the drop of `Exhausting` will cause `next()` to be called again which is not unlikely to produce another panic, resulting in the whole program to abort. We could guard against aborts, by not self-exhausting when the panic occured during iteration (communicated via a flag) or, alternatively, not to self-exhaust under any panic (with `std::thread::panicking()`). This is a choice between possibly unnecessary leaks and a higher likelihood of accidentally tearing down the whole process. Example of how this would look like: ```rust impl<T: Iterator> Iterator for Exhausting<T> { type Item = ...; fn next(...) -> ... { // no additional branching self.currently_iterating = true; // no double iteration let next = self.iter.next(); self.current_iterating = false; // no double iteration } } impl<T: Iterator> Drop for Exhausting<T> { fn drop(&mut self) { // if !std::thread::panicking() { // no panicking iteration at all if !self.currently_iterating { // no double iteration for _ in self.iter {} } } } ```