Skip to content

Instantly share code, notes, and snippets.

@erica
Last active February 23, 2017 00:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erica/05538831e5e0fdca0bc42afc617bd170 to your computer and use it in GitHub Desktop.
Save erica/05538831e5e0fdca0bc42afc617bd170 to your computer and use it in GitHub Desktop.

Eliminating enumerated() from the Standard Library

Introduction

This proposal removes the enumerated() method from the Standard library.

Swift-evolution threads:

Motivation

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 like x.reversed().enumerated(), where x 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

Evaluating enumerated() in the Standard Library

Ben Cohen details a checklist of qualities that support Standard Library integration.

Is this a frequently used operation?

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.

Does this helper measurably improve code vocabulary?

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)

Is this helper performant?

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:

Is the helper inherently flexible across many use-cases?

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.

Does the helper introduce a moral hazard?

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.

Detailed Design and Impact on Existing Code

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) or
  • zip(arr.indices, arr)

Alternatives Considered

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.

@xwu
Copy link

xwu commented Feb 21, 2017

Comments:

  • I rather think that everything between "Moral Hazard" and "Chris Eid[h]of" [whose name is misspelled, incidentally] isn't adding much. I think the quotation from Chris explains the specific dangers very clearly.
  • As I wrote to the list (if you can still find it), it's not true based on a survey of GitHub that 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 of enumerated() to justify its removal.
  • Similarly, we ought to be fair in the upfront analysis and mention, in relation to having many use cases, that enumerated() could easily be extended to have a different starting value, much like Python has done with its enumerate().
  • Detailed design should design the fix-it. It shouldn't be an afterthought in the next section. The fix-it should offer two choices: zip(sequence(first: 0, next: { $0 + 1 }), arr) or zip(arr.indices, arr).

@erica
Copy link
Author

erica commented Feb 21, 2017

I rather think that everything between "Moral Hazard" and "Chris Eid[h]of" [whose name is misspelled, incidentally] isn't adding much. I think the quotation from Chris explains the specific dangers very clearly.

Name fixed, argument moved, structured adapted

As I wrote to the list (if you can still find it), it's not true based on a survey of GitHub that 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 of enumerated() to justify its removal.

Removed.

Similarly, we ought to be fair in the upfront analysis and mention, in relation to having many use cases, that enumerated() could easily be extended to have a different starting value, much like Python has done with its enumerate().

Added ways it could be adapted to introduce fixes.

Detailed design should design the fix-it. It shouldn't be an afterthought in the next section. The fix-it should offer two choices: zip(sequence(first: 0, next: { $0 + 1 }), arr) or zip(arr.indices, arr).

Added

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment