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

Brent Royal-Gordon brent at brentdax.com
Fri Apr 15 16:59:27 CDT 2016

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.) Most people use it because they want to enumerate over indices and elements at the same time. 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

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.

This is made worse by the fact that `enumerate()` is not really a good name. 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.

I have three possible solutions to propose.


* Remove `enumerate()`.

* 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.)

* 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.)

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

* 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.

Brent Royal-Gordon

More information about the swift-evolution mailing list