I just started playing around with Rust with small, simple programs and I had an idea. Maybe it's a stupid idea but I want to mention it anyway so someone can explain, why it is stupid.
So ... what if enumerate() had a parameter for an offset. or even two parameters for offset and step width?
Examples:
for (i, x) in list.enumerate(1) {
// i -> 1, 2, 3, ...
}
for (i, x) in list.enumerate(0, 10) {
// i -> 0, 10, 20, ...
}
I know there is enumerate().map(|(i, x)| (i + 1), x)) for the first version but if people need to use that fairly often, maybe having a shorter version might still be nice.
Thanks for your suggestions. I am certain there are even more ways to solve these problems. What I am aiming at, however, is to improve "ergonomics". And so far I did not see a solution that felt "simple". Maybe I am just not familiar enough with some programming paradigms, but maybe ergonomics could be improved in these cases nevertheless.
I understand the utility, but personally I don't think it would be helpful enough to consider complicating the most common case, even if adding it wouldn't be a breaking change.
If you meant that we should add new methods, e.g. enumerate_by, I think that the discoverability would be about on par with the existing solutions. That said, I wouldn't be opposed to it.
The word "enumerate" isn't really the right name for pairing items with numbers "0, 10, 20" or some other sequence. To "enumerate" something is to make a list numbered with consecutive ordinal numbers (indices) 0, 1, 2, ... If you want to pair items with some other sequence, zip is more readable.
Making enumerate do different things (note that Rust doesn't have function overloading, so it would actually have to be three methods with different names, not one enumerate with three ways to call it!) would violate orthogonality, which is a fancy word for having a set of standard, composable building blocks, each doing one thing distinct from the others. Indeed I'd even argue that enumerate is redundant because zip(list, 0..) is such a simple way to do the same thing.
Fixing this would need some kind of zip_infinite function and InfiniteIterator trait that could always propagate the properties of one side of the zip.
As such, my_iter.enumerate() is less like a zip and more like
my_iter.map({
let mut i = 0;
move |x| {
let temp = i;
i += 1;
(temp, x)
}
});
If there was a way to express that InfiniteIterator and ExactSizeIterator are disjoint traits, zipping InfiniteIterator and ExactSizeIterator could produce an ExactSizeIterator without the need for a separate function.
Alright, thanks for all the feedback. I think we can boil it down to:
we do not need it because we have the building blocks already
-> Coming from an embedded C/C++ background, I had this discussion at least twice for every year of my career and I still do not buy this argument. If we could make something simpler to use, we should at least try. And if you need to be familiar with a certain programming paradigm to understand the code, you will have 25% of people on your team, who do not get it and invent some workaround for stuff that is "missing". ... Also, in C++ it is "just" another overload. I did not know Rust could not do function overloading. And then it comes to...
not widely enough used to put it to the language
-> I am totally in line with this. It is not a good idea to put everything into a language that some dude on the interwebs comes up with. That's why I was asking how many people would want this or maybe missed it already before. But...
arguing that "enumerating" means "a list numbered with consecutive ordinal numbers" is at least not telling the whole story. "Enumerating" does not tell anything about numbers. At least not if you ask linguistics. It just means that you list items one by one. This would still hold true for inconsequent numbering as long as all items were listed. However, this kind of arguing probably cannot solve anything. Many terms used in technology do not hold the same meaning as the same term in conventional linguistics.
Anyway, I take from this that I am not familiar enough with what I assume to be functional paradigm constructs. Coming up with (1..).zip(list.iter()) does not feel natural to me yet. Maybe it will soon.
enumerate() matches this definition (except we like to start natural numbers at 0 rather than 1).
In set theory, there is a more general notion of an enumeration than the characterization requiring the domain of the listing function to be an initial segment of the Natural numbers where the domain of the enumerating function can assume any ordinal. Under this definition, an enumeration of a set S is any surjection from an ordinal α onto S .
It also matches this (here clearly starting with 0 because ordinals unambiguously start with 0).
TBH, I don't think I'd mind an .enumerate_from(5_u32) or similar. The amount of extra code for it in the library is tiny, since it could just be based on where RangeFrom<T>: Iterator, and give the existing iter::Enumerate type a second type generic that defaults to usize.
After all, the Enumerate struct is already storing the value to return the next time you call next, so letting you set that wouldn't be unreasonable.
There's already plenty of duplicative code in std::iter, and sometimes clippy will warn you when you reimplement something in std (e.g. map_filter). Do we need sum when we have fold? Should everything implemented in terms of try_fold be eliminated in favor of it?
It's a trade-off. enumerate_from seems a lot less useful than sum, since you normally want to enumerate things from 0, and especially since zip(5u32..) already exists and is shorter than enumerate_from(5u32).