Skip to content

Add Iterator::collect_into #2339

@CodeSandwich

Description

@CodeSandwich

Summary

Add Iterator::collect_into method, which takes a collection and extends it with iterator’s content

Motivation

Ergonomics

Sometimes there is a need to create a sophisticated iterator and then put all it's items into a collection. Of cource there is Iterator::collect, which creates a new collection, but there is no convenient method of collecting into an EXISTING one.

Sure, it's possible, but a bit awkward:

fn my_fn(&self, vec: &mut Vec) {
    let iter = self.items.iter()
        .filter(...)
        .enumerate()
        .map(...);
    vec.extend(iter);
}

or more compact, but way harder to follow:

fn my_fn(&self, vec: &mut Vec) {
    vec.extend(self.items.iter()
        .filter(...)
        .enumerate()
        .map(...)
    );
}

but nothing beats THIS:

fn my_fn(&self, vec: &mut Vec) {
    self.items.iter()
        .filter(...)
        .enumerate()
        .map(...);
        .collect_into(vec);
}

collect_into could return the extended collection:

fn my_fn(&self, vec: &mut Vec) -> usize {
    self.items.iter()
        .filter(...)
        .enumerate()
        .map(...);
        .collect_into(vec)
        .len()
}

the function could accept collections by both reference or value:

fn my_fn(&self, mut vec: Vec) -> Vec {
    self.items.iter()
        .filter(...)
        .enumerate()
        .map(...);
        .collect_into(vec)
}

Manually size-hinted collect

This use case was invented by @stuhood in Rust issue 45840.

As originally determined in https://twitter.com/kot_2010/status/927119253324619776), building a pre-size-hinted mutable Vec as the output of a loop (even for very small n) can occasionally be more than twice as fast as collecting into a Vec. As demonstrated here , using extend into a pre-size-hinted Vec closes most of the gap.

Focusing on improving Iterator::size_hint implementations so that they suggest better sizes for collect would likely help reduce this gap. But there are diminishing returns to heuristics, and in many cases users should be able to give better explicit hints.

Guide-level explanation

Iterator::collect_into takes all items from the Iterator and adds them to an existing collection. It's a counter-part for Extend::extend, but instead of being invoked on container, it's called on Iterator making it's usage similar to Iterator::collect.

let mut vec = vec![0, 4];
(3..7)
    .filter(|i| i % 2 == 0)
    .map(|i| i * 2);
    .collect_into(&mut vec);
assert_eq!(vec![0, 4, 8, 12], vec);
}

Iterator::collect_into takes collection either by mutable reference or value. It also returns the extended collection making chaining elegant.

let vec = (2..4)
    .map(|i| i * 2)
    .collect_into(vec![0, 2])
    .windows(2)
    .map(|c| c[0] + c[1])
    .collect::<Vec<_>>();
assert_eq!(vec![2, 6, 10], vec);

Reference-level explanation

No rocket science here.

pub trait Iterator {
    type Item;

    ...

    fn collect_into<B, E>(self, extended: B) -> B
    where
        B: BorrowMut<E>,
        E: Extend<Self::Item> {
        extended.borrow_mut().extend(self);
        extended
     }
}

The BorrowMut allows passing collections either by value or by reference, which makes it much more elastic.

Drawbacks

This proposition makes Iterator interface even broader without extending its functionality significantly, it's just ergonomics.

Rationale and alternatives

This proposition removes a small stdlib papercut making it more pleasant and expressive. If it's not added, people will just keep using workarounds with Extend::extend.

Unresolved questions

None, that I'm aware of.

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiRelevant to the library API team, which will review and decide on the RFC.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions