- Proposal: TBD
- Author: Ben Cohen, Chris Eidhof, Nate Cook, Jacob Bandes-Storch, Erica Sadun
- Status: TBD
- Review manager: TBD
This proposal removes the enumerated()
method from the Standard library.
Swift-evolution threads:
The Standard Library's enumerated()
method fails to pull its weight as a core Swift operation. The method is confusing and poorly specified. Chris Eidhof describes how Swift learners respond to enumerated()
:
"In almost every workshop, there's someone who thinks that
enumerated()
returns a list of(index, element)
pairs. This is only true for arrays. It breaks when using array slices, or any other kind of collection. In our workshops, I sometimes see people doing something likex.reversed().enumerated()
, wherex
is an array, and somehow it produces behavior they don't understand."
Indices have a specific fixed meaning in Swift. They are used to create valid collection subscripts. The enumerated()
method returns a sequence of pairs counting a sequence, starting at zero and increasing monotonically thereafter. The starting value cannot be adjusted and the count may not semantically coordinate with indices.
// with `enumerated()`
for (i, value) in arr.enumerated() { ... }
for (i, value) in slice.enumerated() { ... } // possible wrong indices
for (i, value) in dict.enumerated() { ... } // makes no sense
Ben Cohen details a checklist of qualities that support Standard Library integration.
The Standard Library offers a curated selection of helper functionality. Helpers you cannot find do no good. APIs should not bristle with methods like an over-decorated Christmas tree. An over-populated API is bad for discoverability and usability. String+Foundation, for example, currently includes over 200 methods/properties. Keeping helper-method growth under control is a core language concern.
A well-designed helper operation conveys its fully composed equivalent at a glance. It is well named, offers obvious functionality, lowers coding overhead, and enhances readability without hiding fundamental operations or discouraging users from discovering functionality like indices
and zip()
. enumerated()
fails these measures. Its fully composed version is more comprehensible than the helper functionality:
for (count, value) in zip(sequence(first: 0, next: { $0 + 1 }), arr) { ... }
for (index, value) in zip(arr.indices, arr)
A good helper avoids inherent performance traps, such as having to remember to introduce lazy
to avoid creating temporary arrays. Helper methods should, where possible, lift performance burden details from the user and provide an efficient usage experience.
A good helper leverages internal implementation details to provide greater efficiency than the equivalent composed form. For example, Dictionary.mapValues(transform: (Value)->T)
can re-use the hash table storage layout in the new dictionary, even when the Value
type changes.
While enumerated()
avoids advancing an index twice for each iteration, you can loop over indices
and use the resulting index values to access each element.
see:
A good helper is multifunctional, across many types, with specialization features. enumerated()
cannot start from 1, does not work in lock-step with slices, cannot be used with non-integer indices, makes no sense with dictionary types.
enumerated()
could be updated to allow optional arguments influenced from other languages like Python:
- Allow caller-controlled starting values.
- Match the enumeration to slicing values.
- Allow the enumeration to follow existing indices.
A good helper does not encourage misuse, as in the following example:
for (index, _) in collection.enumerated() {
// mutate collect[index] in-place
// i.e. collection[index] += 1
}
The enumerated()
method introduces an "attractive nuisance" when callers mistake the monotonically zero-based count for indices when working with slices or non-Array sequences.
Upon adoption, enumerated()
is removed from the Standard Library. This proposal breaks existing enumerated()
use. A fixit will offer to replace enumerated()
with a composed equivalent:
zip(sequence(first: 0, next: { $0 + 1 }), arr)
orzip(arr.indices, arr)
We first considered introducing indexed()
(or enumeratedByIndex()
), a method on collections that returns an (index, element) tuple sequence or introducing a variant of makeIterator()
. On-list discussion suggests the indexed()
method does not meet the Standard Library quality checklists. For example, indexed()
is just as easily composed as abstracted. It does not measurably improve reading comprehension or cognitive chunking. A native version of indexed()
may offer a slight performance improvement over zip()
as zip()
calculates indices twice for any collection that uses IndexingIterator
as its iterator. For collections that do not, it performs an equivalent in calculating an index offset along with whatever work the Iterator does to calculate the next element.
Comments:
enumerated()
is rarely used or that it's often used incorrectly in that code. It's used in most of the top 20 Swift projects, and in those projects I found only one potentially erroneous use. I think it does readers a disservice to exaggerate the deficiencies ofenumerated()
to justify its removal.enumerated()
could easily be extended to have a different starting value, much like Python has done with itsenumerate()
.zip(sequence(first: 0, next: { $0 + 1 }), arr)
orzip(arr.indices, arr)
.