[swift-evolution] [Idea] Replace enumerate() with something more explicit

Dave Abrahams dabrahams at apple.com
Tue Apr 19 17:16:21 CDT 2016

on Fri Apr 15 2016, Brent Royal-Gordon <swift-evolution at swift.org> wrote:

> A discussion in the "mapValues" thread reminded me of a longstanding issue I have with Swift.
> `enumerate()` is an attractive nuisance. (That is, it looks like the
> thing you want, but it's not actually the right one.) 

It depends what you want.  It exists in part because it's nontrivial to
write a method that does what enumerate does, and does it efficiently.

> Most people use it because they want to enumerate over indices and
> elements at the same time. 

If what you want is zip(a.indices, a), it's easy enough to write that.

> In reality, though, the first element of an
> `enumerate()` tuple is not the index—it's a monotonically increasing
> integer starting at 0. That *happens* to work for `Array`:
>> 	  1>     let array = Array(0..<10) 
>> 	  2.     for (i, elem) in array.enumerate() { 
>> 	  3.         print(array[i]) 
>> 	  4.     }
>> 	0
>> 	1
>> 	2
>> 	3
>> 	4
>> 	5
>> 	6
>> 	7
>> 	8
>> 	9
> But if you stray even a little bit from the golden path, things start to go wrong:
>> 	  5>     let range = array[2..<8]
>> 	  6.     for (i, elem) in range.enumerate() { 
>> 	  7.         print(range[i])
>> 	  8.     } 
>> 	fatal error: Index out of bounds

I think the potential for this confusion is inherent in the combination
of two factors:

* Arrays are indexed with integers

* The indices of a SubSequence are the indices of corresponding elements
  in the thing it was sliced from.

I also think it has nothing whatsoever to do with enumerate.

> You can scarcely blame users for making this mistake, though—"The
> Swift Programming Language" encourages the misconception. "Iterating
> Over an Array" in "Collection Types":
>> If you need the integer index of each item as well as its value, use
>> the `enumerate()` method to iterate over the array instead. For each
>> item in the array, the `enumerate()` method returns a tuple composed
>> of the index and the value for that item.
> While the text is technically accurate—it only talks about iterating
> over arrays and the numbers generated by `enumerate()` happen to
> correspond to array indices—it creates a false implication that
> `enumerate()` is defined to return indices, which isn't true of other
> collections.

You should consider filing a bug report against TSPL, I think.

> This is made worse by the fact that `enumerate()` is not really a good
> name. 

Totally open to better names.  It's precedented in Python with exactly
these semantics, which is why we used it.

> It is not a common word, so people don't read it and immediately
> understand what it does; they memorize a Swift-specific meaning, and
> that meaning may incorporate the misconception that `enumerate()`
> includes indices. It is also not technically accurate: although it has
> "number" in its Latin roots, "enumerate" means either "to count" or
> "to list", not "to number" (i.e. assign numbers to). I know
> `enumerate()` is used in a couple of other languages (certainly
> Python, possibly others), but I don't think that overrides the fact
> that it's confusing.

Maybe "numbered()" would be a better name.

> I have three possible solutions to propose.
> * Remove `enumerate()`.

I'm open to doing this if nobody feels the current semantics are useful
and we can get some real-world data showing that they're not.

> * Provide a type and postfix operator which allows you to write an
> infinite sequence as `0...`. (This might call a ClosedRange
> constructor which constrains the type to a protocol which provides
> `max`. This would imply that `FloatingPoint` and `Integer` protocols
> would need to conform to a common protocol declaring a `max` property,
> and in the case of `FloatingPoint`, `max` should probably be positive
> infinity.)

+1; I want this feature “anyway.”  I also want to use it for slicing,
e.g. in lieu of x.dropFirst(3), x[3...]

> * Fix-It calls to `x.enumerate()` as `zip(0..., x)`. This is more
> complicated to look at, but it's *far* more explicit about what the
> operation actually does. (For bonus points, we could perhaps look at
> how the EnumerateSequence is used, and if the number is used as an
> index, go with `zip(x.indices, x)` instead.)
> * Rename `enumerate()` to something more explicit, like
> `withIntegers()` or `numbered()`. (It might even make sense to add a
> `from:` parameter which defaults to 0.)

Hey (if we keep it) good idea!

> * Add to Collection an equivalent method with a similar name that
> provides indices, like `withIndices()` or `indexed()`.

Less enthused about this one, since you can get it trivially by
combining primitives.

> * Fix-It calls to `enumerate()` into either `numbered()` or
> `indexed()` (or whatever they end up being called) depending on the
> way they are used.
> Combine the other two options:
> * Provide the infinite numeric sequence type from Option One.
> * Provide `numbered()` and `indexed()` (or whatever) methods which are
> explicitly typed to merely zip over the sequence/collection with an
> infinite integer sequence/Indices collection.


More information about the swift-evolution mailing list