Skip to content

ACP: Uplift iter::repeat_n from itertools #120

Closed
@scottmcm

Description

@scottmcm

Proposal

Problem statement

Add the repeat_n<T>(value: T, n: usize) -> RepeatN<T> function and iterator type to iter.

This exists as itertools::repeat_n, but can easily be uplifted to core+std, like how iter::repeat_with has obviated itertools::repeat_call.

(This is a function, not a trait method, so doesn't have the resolution issues that make this hard for things like intersperse.)

Motivation, use-cases

The obvious alternative here is iter::repeat(value).take(n). But there's two gotchas with that approach:

It has to always clone value, never move it

If it's a string, for example, that buffer can never be reused (other than in specializations) since it needs to always keep the value around for the potentially-next iteration. (And even if it somehow knew it was the last element needed, it still can't move it in non-consuming calls as it'd make a follow-up .next() call UB, and the Drop would double-free.)

Vec has the secret from_elem for vec! to use to overcome this problem, as well as a bunch of extra internal stuff like ExtendElement for other places that want this functionality.

But other containers don't have this. For example,

iter::repeat(my_string).take(10).collect::<VecDeque<String>>()

will do 10 clones (instead of the 9 that are really needed) and there's no nice way to avoid that.

Even VecDeque::resize didn't bother doing better here, as it just calls .resize_with(n, || value.clone()), which will also do the extra clone.

Thus one would need to do some long dance like

let mut v = VecDeque::with_capacity(10);
v.extend(iter::repeat(&my_string).cloned().take(9));
v.push_back(my_string);

and that's enough of a pain that it's almost certainly not happening.

It would be nice to be able to just do iter::repeat_n(my_string, n).collect() and have that work great in containers, rather than needing them all to add from_elem-like methods or recreate extend specialization infrastructure for all of them like Vec is doing.

It's not ExactSizeIterator.

While that does have a perfect size_hint, iter::Repeat<_>: !ExactSizeIterator (and must not be) and thus the Take<…> isn't either:

iter::repeat(1).take(10).len();

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=74ea40842f56f87224a4bb2411967ec6

  |     iter::repeat(1).take(10).len();
  |                              ^^^ method cannot be called on `std::iter::Take<std::iter::Repeat<{integer}>>` due to unsatisfied trait bounds
  |
  = note: the following trait bounds were not satisfied:
          `std::iter::Repeat<{integer}>: ExactSizeIterator`
          which is required by `std::iter::Take<std::iter::Repeat<{integer}>>: ExactSizeIterator`

That could be made to work, but the "obvious" way would need both an InfiniteIterator trait and some #[marker] traits to express the bound, which combined make a pretty big hammer.

Whereas RepeatN<_> is non-infinite -- limited by the usize -- so is naturally ExactSizeIterator. Thus even for things like repeat(0).take(n) where buffer reuse is irrelevant, this is still useful. (Such as in -> impl ExactSizeIterator<Item = …> APIs.)

Solution sketches

So long as cloneing in this isn't considered a side effect, it can be made to work nicely. That should be uncontroversial, as the existing repeat doesn't either -- for example iter::repeat(NoisyClone).nth(10) only clones once: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b8455030a83615d6b4a1b92c25d65571

A straight-forward and natural implementation even allows LLVM to unify the "clone" and "move the last time" branches for trivial-to-clone types even if they're not Copy -- with no special needs_drop checks or specialization: https://rust.godbolt.org/z/drjzsdfoe

Links and related work

As mentioned, https://docs.rs/itertools/latest/itertools/fn.repeat_n.html

The suffix _n for "counted" versions of methods is used in Elements of Programming and common in C++ functions like copy_n.

What happens now?

This issue is part of the libs-api team API change proposal process. Once this issue is filed the libs-api team will review open proposals in its weekly meeting. You should receive feedback within a week or two.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ACP-acceptedAPI Change Proposal is accepted (seconded with no objections)T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions