[Draft] Rationalizing Sequence end-operation names


(Brent Royal-Gordon) #1

As previously threatened mentioned, I've written a draft proposal to fix a number of naming issues with APIs operating on the beginning and end of Sequences and Collections:

• Inconsistent use of `prefix`/`suffix` vs. `first`/`last`
• Confusing naming of `drop` methods
• Ambiguous naming of `index(of:/where:)` and `drop(while:)`
• `prefix(upTo:)`, `prefix(through:)`, and `suffix(from:)` shouldn't be part of this family at all

To fix this, I propose:

• Renaming all methods which operate on more than one element at the beginning/end to use "prefix" or "suffix", not "first" or "last"
• Renaming `index(of:/where:)` to `earliestIndex(…)` and `first(where:)` to `earliest(where:)`
• Renaming the `drop` methods to use `removing`
• Redesigning `prefix(upTo:)`, `prefix(through:)` and `suffix(from:)` as subscripts with "partial" ranges, like `people[..<idx]` or perhaps `people[nil..<idx]`.

Since that last point requires significant redesign, including the introduction of new types, I have also included an alternative design which uses `people[to: idx]` instead.

This proposal does not seek to add new functionality; it merely renames or (in the case of the "aggressive" subscript option) redesigns existing functionality. I do, however, discuss (without making many judgements about their wisdom) how these changes might affect the naming of functionality we might add in future versions of Swift.

I would mainly like feedback on the two most open questions left in this proposal:

• The choice of `removing` to replace `drop`
• The decision about whether to use `people[..<idx]`, `people[nil..<idx]`, or `people[to: idx]`.

But I'd also like comments on the rest of the proposal, and on whether I should split the prefix(upTo:/through:)/suffix(from:) changes into a separate proposal from the rest.

I suspect this will cause a firestorm of bikeshedding, so please try to keep your suggestions grounded. Don't just suggest a name; articulate why it's a better choice than what we already have or what this proposal suggests. Only you can prevent our first *three*-hundred-message bikeshedding thread.

Thanks for your attention!

(P.S. The proposal below includes several huge tables which may cause some mail clients to become very pouty and refuse to eat their supper. You may want to read the proposal at <https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430> instead.)

Rationalizing Sequence end-operation names
Proposal: SE-NNNN <https://gist.github.com/brentdax/NNNN-sequence-end-op.md>
Author: Brent Royal-Gordon <https://github.com/brentdax>
Status: Draft
Review manager: TBD
<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#introduction>Introduction

Sequence and Collection offer many special operations which access or manipulate its first or last elements, but they are plagued by inconsistent naming which can make it difficult to find inverses or remember what the standard library offers. I propose that we standardize these names so they follow consistent, predictable patterns.

Swift-evolution thread: Pending <http://news.gmane.org/gmane.comp.lang.swift.evolution>
<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#scope>Scope

This proposal is not intended to add or remove any functionality; it merely renames and redesigns existing operations. Adding new operations is out of scope for this proposal unless it's incidental to the new designs.

Nonetheless, I do want the new designs to support adding more operations in the future. The names I've chosen are informed by some of the speculative APIs discussed in "Future directions", although I think they are perfectly sensible names even if nothing else changes.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#motivation>Motivation

The Sequence and Collection protocols offer a wide variety of APIs which are defined to operate on, or from, one end of the sequence:

Operand Get Index Exclude Remove (1) Pop (1) Equate (2)
Fixed Size
First 1 C.first - S.dropFirst() C.removeFirst() C.popFirst() -
Last 1 C.last - S.dropLast() C.removeLast() C.popLast() -
First (n: Int) S.prefix(_:slight_smile: - S.dropFirst(_:slight_smile: C.removeFirst(_:slight_smile: - S.starts(with:)
  ...with closure S.prefix(while:) - S.drop(while:) - - S.starts(with:isEquivalent:)
Last (n: Int) S.suffix(_:slight_smile: - S.dropLast(_:slight_smile: C.removeLast(_:slight_smile: - -
  ...with closure - - - - - -
Searching From End
First matching element - C.index(of:) - - - -
  ...with closure S.first(where:) C.index(where:) - - - -
Last matching element - - - - - -
  ...with closure - - - - - -
Based on Index
startIndex ..< (i: Index) C.prefix(upTo:) - - - - -
startIndex ... (i: Index) C.prefix(through:) - - - - -
(i: Index) ..< endIndex C.suffix(from:) - - - - -
I have included several blank rows for operands which fit the APIs' patterns, even if they don't happen to have any operations currently.

Type abbreviations:

S = Sequence
C = Collection (or a sub-protocol like BidirectionalCollection)
Notes:

remove and pop both mutate the array to delete the indicated element(s), but remove assumes as a precondition that the indicated elements exist, while pop checks whether or not they exist.

String and NSString have bespoke versions of first n and last n Equate operations, in the form of their hasPrefix and hasSuffix methods.

Leaving aside the question of whether any gaps in these tables ought to be filled, I see a number of issues with existing terminology.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#inconsistent-use-of-prefix-and-suffix>Inconsistent use of prefix and suffix

Some APIs which operate on a variable number of elements anchored at one end or the other use the terms prefix or suffix:

Sequence.prefix(_:slight_smile: and Sequence.suffix(_:slight_smile:
Sequence.prefix(while:)
String.hasPrefix(_:slight_smile: and String.hasSuffix(_:slight_smile:
Others, however, use first or last:

Sequence.dropFirst(_:slight_smile: and Sequence.dropLast(_:slight_smile:
Sequence.removeFirst(_:slight_smile: and Sequence.removeLast(_:slight_smile:
Still others use neither:

Sequence.starts(with:)
Sequence.drop(while:)
These methods are all closely related, but because of this inconsistent terminology, they fail to form predictable method families.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#first-has-multiple-meanings>first has multiple meanings

The word first can mean three different things in these APIs:

Just the very first element of the sequence.

A subsequence of elements anchored at the beginning of the sequence, as mentioned in the last point.

The first element encountered in the sequence which matches a given criterion when walking from the beginning of the sequence towards the end.

It would be nice to have more clarity here.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#drop-is-misleading-and-scary>drop is misleading and scary

In a Swift context, I believe the drop methods are actively confusing:

drop does not have the -ing or -ed suffix normally used for a nonmutating method.

drop has strong associations with destructive operations; it's the term used, for instance, for deleting whole tables in SQL. Even dropping would probably sound more like a mutating operation than alternatives.

As previously mentioned, the use of dropFirst and dropLast for single-drop operations and multiple-drop operations breaks up method families.

drop, dropFirst, and dropLast are terms of art, so we allow them a certain amount of leeway. However, I believe the drop functions go well beyond what we should permit. They are relatively uncommon operations, associated primarily with functional languages rather than mainstream object-oriented or imperative languages, and their violation of the normal Swift naming guidelines is especially misleading.

The term-of-art exception is not a suicide pact; it is meant to aid understanding by importing common terminology, not bind us to follow every decision made by any language that came before us. In this case, I think we should ignore precedent and forge our own path.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#unstated-direction-of-operation>Unstated direction of operation

Several APIs could theoretically be implemented by working from either end of the sequence, and would return different results depending on the direction, but do not indicate the direction in their names:

Sequence.drop(while:)
Collection.index(of:)
Adding a direction to these APIs would make their behavior clearer and permit us to offer opposite-end equivalents in the future. (Unmerged swift-evolution pull request 329 <https://github.com/apple/swift-evolution/pull/329> would add lastIndex methods.)

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#the-index-base-name-has-been-polluted>The index(...) base name has been polluted

Swift 3's new collection model placed a number of low-level index manipulating operations on the base method name index. These now share that name with index(of:) and index(where:), which are much higher-level operations. This may be confusing for users looking for high-level operations; the only real relationship between the two sets of operations is that they both return an index.

It would be nice to separate these two groups of methods into different families.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#operations-taking-an-index-are-really-slicing>Operations taking an index are really slicing

prefix(upTo:), prefix(through:), and suffix(from:) at first appear to belong to the same family as the other prefix and suffix methods, but deeper examination reveals otherwise. They are the only operations which take indices, and they don't cleanly extend to the other operations which belong to these families. (For instance, it would not make sense to add a dropPrefix(upTo:) method; it would be equivalent to suffix(from:).)

Also, on Int-indexed collections like Array, prefix(_:slight_smile: and prefix(upTo:) are identical, but there is little relationship between suffix(_:slight_smile: and suffix(from:), which is confusing.

suffix(from:) is a particularly severe source of confusion. The other suffix APIs all have parameters relative to the endof the collection, but suffix(from:)'s index is still relative to the beginning of the array. This is obvious if you think deeply about the meaning of an index, but we don't really want to force our users to stare at a strange API until they have an epiphany.

I believe these operations have much more in common with slicing a collection using a range, and that reimagining them as slicing APIs will be more fruitful.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#why-does-it-matter>Why does it matter?

Many of these APIs are only occasionally necessary, so it's important that they be easy to find when needed and easy to understand when read. If you know that prefix(10) will get the first ten elements but don't know what its inverse is, you will probably not guess that it's dropFirst(10). The confusing, conflicting names in these APIs are a barrier to users adopting them where appropriate.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#proposed-solution>Proposed solution

We sever the index-taking APIs from the others, forming two separate families, which I will call the "Sequence-end operations" and the "index-based operations". We then consider and redesign them along separate lines.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#sequence-end-operations>Sequence-end operations

Each of these APIs should be renamed to use a directional word based on its row in the table:

Operand Directional word
Fixed Size
First 1 first
Last 1 last
First (n: Int) prefix
  ...with closure prefix
Last (n: Int) suffix
  ...with closure suffix
Searching From End
First matching element earliest
  ...with closure earliest
Last matching element latest
  ...with closure latest
To accomplish this, starts(with:) should be renamed to hasPrefix(_:), and other APIs should have directional words replaced or added as appropriate.

Additionally, the word drop in the "Exclude" APIs should be replaced with removing. These operations omit the same elements which the remove operations delete, so even though the types are not always the same (removing returns SubSequence, not Self), I think they are similar enough to deserve to be treated as nonmutating forms.

These changes yield (altered names bold):

Operand Get Index Exclude Remove (1) Pop (1) Equate (2)
Fixed Size
First 1 C.first - S.removingFirst() C.removeFirst() C.popFirst() -
Last 1 C.last - S.removingLast() C.removeLast() C.popLast() -
First (n: Int) S.prefix(_:slight_smile: - S.removingPrefix(_:slight_smile: C.removePrefix(_:slight_smile: - S.hasPrefix(_:slight_smile:
  ...with closure S.prefix(while:) - S.removingPrefix(while:) - - S.hasPrefix(_:isEquivalent:)
Last (n: Int) S.suffix(_:slight_smile: - S.removingSuffix(_:slight_smile: C.removeSuffix(_:slight_smile: - -
  ...with closure - - - - - -
Searching From End
First matching element - C.earliestIndex(of:) - - - -
  ...with closure S.earliest(where:) C.earliestIndex(where:) - - - -
Last matching element - - - - - -
  ...with closure - - - - - -
<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#alternative-to-removing>Alternative to removing

If the type differences are seen as disqualifying removing as a replacement for drop, I suggest using skipping instead.

There are, of course, many possible alternatives to skipping; this is almost a perfect subject for bikeshedding. I've chosen skipping because:

It is not an uncommon word, unlike (say) omitting. This means non-native English speakers and schoolchildren are more likely to recognize it.

It is an -ing verb, unlike (say) without. This makes it fit common Swift naming patterns more closely.

It does not imply danger, unlike (say) dropping, nor some sort of ongoing process, unlike (say) ignoring. This makes its behavior more obvious.

If you want to suggest an alternative on swift-evolution, please do not merely mention a synonym; rather, explain why it is an improvement on either these axes or other ones. (I would be particularly interested in names other than removing which draw an analogy to something else in Swift.)

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#index-based-operations>Index-based operations

Because these APIs look up elements based on their indices, I believe these operations should be exposed as subscripts, and ideally should look like other slicing operations.

My primary design is rather ambitious, introducing two new types and either two operator overloads, or four unary forms of existing binary operators. I therefore present a more conservative alternative as well.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#preferred-ambitious-option>Preferred (ambitious) option

let head = people[..<i]
let tail = people[i..<]
let rearrangedPeople = tail + head
Or this small variation:

let head = people[nil ..< i]
let tail = people[i ..< nil]
let rearrangedPeople = tail + head
The operators would construct instances of a new pair of types, IncompleteRange (for ..<) and IncompleteClosedRange (for ...), and Collection would include new subscripts taking these types. These would probably have default implementations which constructed an equivalent Range or ClosedRange using startIndex and endIndex, then passed the resulting range through to the existing subscripts.

I prefer this option because it offers an elegant syntax immediately recognizable as a form of slicing, and provides a straightforward way for a future version of Swift to extend other Range-handling Collection operations, like replaceSubrange(_:with:) and removeSubrange(_:), to handle subranges bound by the ends of the Collection.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#alternative-conservative-option>Alternative (conservative) option

let head = people[to: i]
let tail = people[from: i]
let rearrangedPeople = tail + head
This would be a simple changing of the methods into subscripts; no additional types or operators would be needed. I have changed upTo: into just to: to match the pattern set by stride(from:to:by:).

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#detailed-design>Detailed design

Draft note: This section is a mere sketch and should probably be expanded before submitting for a pull request.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#sequence-end-operations-1>Sequence-end operations

The following methods should be renamed as follows wherever they appear in the standard library. These are simple textual substitutions; we propose no changes whatsoever to types, parameter interpretations, or other semantics.

Old method New method
dropFirst() -> SubSequence removingFirst() -> SubSequence
dropLast() -> SubSequence removingLast() -> SubSequence
dropFirst(_ n: Int) -> SubSequence removingPrefix(_ n: Int) -> SubSequence
drop(@noescape while predicate: (Iterator.Element) throws -> Bool) rethrows -> SubSequence removingPrefix(@noescape while predicate: (Iterator.Element) throws -> Bool) rethrows -> SubSequence
dropLast(_ n: Int) -> SubSequence removingSuffix(_ n: Int) -> SubSequence
removeFirst(_ n: Int) removePrefix(_ n: Int)
removeLast(_ n: Int) removeSuffix(_ n: Int)
starts<PossiblePrefix: Sequence>(with possiblePrefix: PossiblePrefix) -> Bool where ... hasPrefix<PossiblePrefix: Sequence>(_ possiblePrefix: PossiblePrefix) -> Bool where ...
starts<PossiblePrefix : Sequence>(with possiblePrefix: PossiblePrefix, isEquivalent: @noescape (Iterator.Element, Iterator.Element) throws -> Bool) rethrows -> Bool where ... hasPrefix<PossiblePrefix : Sequence>(_ possiblePrefix: PossiblePrefix, isEquivalent: @noescape (Iterator.Element, Iterator.Element) throws -> Bool) rethrows -> Bool where ...
first(where predicate: @noescape (Iterator.Element) throws -> Bool) rethrows -> Iterator.Element? earliest(where predicate: @noescape (Iterator.Element) throws -> Bool) rethrows -> Iterator.Element?
index(of element: Iterator.Element) -> Index? earliestIndex(of element: Iterator.Element) -> Index?
index(where predicate: @noescape (Iterator.Element) throws -> Bool) rethrows -> Index? earliestIndex(where predicate: @noescape (Iterator.Element) throws -> Bool) rethrows -> Index?
<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#index-based-operations-1>Index-based operations

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#preferred-ambitious-option-1>Preferred (ambitious) option

Implementation would be roughly as follows:

Add a pair of new types, IncompleteRange and IncompleteClosedRange, with definitions like:

struct IncompleteRange<Bound: Comparable> {
   var lowerBound: Bound?
   var upperBound: Bound?

   func completed(with bounds: Range<Bound>) -> Range<Bound> {
       return (lowerBound ?? bounds.lowerBound) ..< (upperBound ?? bounds.upperBound)
   }
}
// And likewise for `IncompleteClosedRange`
Either add prefix and postfix ..< and ... operators, or overload the existing infix ..< and ... operators, to construct an IncompleteRange or IncompleteClosedRange. For the prefix/postfix operators, this would look like:

postfix func ..< <Bound: Comparable>(lowerBound: Bound) -> IncompleteRange<Bound> {
   return IncompleteRange(lowerBound: lowerBound, upperBound: nil)
}
prefix func ..< <Bound: Comparable>(upperBound: Bound) -> IncompleteRange<Bound> {
      return IncompleteRange(lowerBound: nil, upperBound: upperBound)
}
// And likewise for `...` and `IncompleteClosedRange`
Add a pair of IncompleteRange and IncompleteClosedRange subscripts to Collection:

extension Collection {
   subscript (range: IncompleteRange<Index>) -> SubSequence {
       return self[range.completed(with: startIndex ..< endIndex)]
   }
   // And likewise for `IncompleteClosedRange`
}
<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#alternative-conservative-option-1>Alternative (conservative) option

The three methods below will be replaced with equivalent subscripts in all types conforming to Collection or a sub-protocol:

Old method New subscript
prefix(upTo end: Index) -> SubSequence subscript(to end: Index) -> SubSequence
prefix(through position: Index) -> SubSequence subscript(through position: Index) -> SubSequence
func suffix(from start: Index) -> SubSequence subscript(from start: Index) -> SubSequence
<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#impact-on-existing-code>Impact on existing code

Obviously, any code using these APIs under their old names or designs would have to be transitioned to the new names and designs.

The sequence-end operations would be by far the simplest to handle; these are simple renamings and could be handed by @available(renamed:) and migration support. The only complication is that some overloads have transitioned to a new base name, while others have stayed with the old one, but I suspect the migrator is up to this task.

The preferred option for index-based operations is more difficult to migrate. The patterns would be roughly:

collection.prefix(upTo: i) => collection[..<i] or collection[nil ..< i]
collection.prefix(through: i) => collection[...i] or collection[nil ... i]
collection.suffix(from: i) => collection[i..<] or collection[i ..< nil]
A custom fix-it would be ideal, but is probably not absolutely necessary here; an @available(message:) would do in a pinch. Presumably this would have to be a special case in the migrator as well.

The alternative option for index-based operations would be simpler to fix-it and migrate—we need merely change references from the method to the subscript with the same (or similar, for upTo) parameter labels. I'm not sure if @available(renamed:) supports changing a method to a subscript.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#alternatives-considered>Alternatives considered

I considered many alternatives to removing or skipping, some of which are discussed in the section about them.

Rather than using earliest and latest, I considered using first and last for the "First matching" and "Last matching" categories. However, I didn't like the thought of overloading a property with a function, and I felt like earliest and latest actually have different semantics (searching for the match closest to a particular end) than first and last (manipulating the element at that end).

I considered using first and last as the basis for both single-element and multiple-element operations (such that prefix(3) would become first(3), etc.), but:

These seemed like distinct functionalities, particularly since their types are different.

I'm not comfortable with heavily overloading a property with a bunch of methods, and didn't want to make firstand last into methods.

Most APIs work fine, but hasFirst(_:slight_smile: is atrocious, and I see no better alternative which includes the word first.

I considered moving first and last to Sequence and possibly making them methods, but my understanding is that the core team has considered and rejected this approach in the past.

I considered moving removingFirst and removingLast to Collection and making them properties, to match firstand last, but this seemed like the sort of foolish consistency that Ralph Waldo Emerson warned of.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#future-directions>Future directions

Note: The rest of this proposal is highly speculative and there's probably no need to read further.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#other-sequence-api-cleanups>Other Sequence API cleanups

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#seriously-source-breaking>Seriously source-breaking

There is an ongoing discussion about which, if any, of map, flatMap, filter, and reduce ought to be renamed to more closely match Swift naming conventions. There is also discussion about relabeling many closure parameters.

The "Future directions" section below suggests every(where:) as an alternative to filter which could be extended in ways compatible with this proposal.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#significantly-source-breaking>Significantly source-breaking

The removeSubrange(_:slight_smile: and replaceSubrange(_:with:) APIs are rather oddly named. They might be better renamed to, for instance, remove(in:) and replace(in:with:).

It is not clear how important removingFirst() and removingLast() actually are, given that they're exactly equivalent to removingPrefix(1) and removingSuffix(1), and their corresponding "get" members are on Collection instead of Sequence. They could be removed.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#slightly-source-breaking>Slightly source-breaking

removeFirst/Last() and popFirst/Last() are very nearly redundant; their only difference is that the removemethods have a non-Optional return type and require the collection not be empty, while the pop methods have an Optional return type and return nil if it's empty.

These operations could be merged, with the remove operations taking on the preconditions of the current popoperations; additionally, removePrefix(_:slight_smile: and removeSuffix(_:slight_smile: could drop their equivalent preconditions requiring that the elements being removed exist. These changes would simplify the standard library and make these methods more closely parallel the equivalent removing methods, which do not have similar preconditions.

Performance-critical code which wants to avoid the checks necessary to remove these preconditions could switch to remove(at:) and removeSubrange(_:), which would continue to reject invalid indices.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#adding-sequence-and-collection-operations>Adding sequence and collection operations

This exercise in renaming suggests all sorts of other APIs we might add, and a few we might rename.

In general, I have not attempted to carefully scrutinize the usefulness of each of these APIs; instead, I have merely listed the ones which I can imagine some kind of use for. The main exception is the "Pop" operation; I can imagine several different, and rather incompatible, ways to extend it, and I'm not going to take the time to sort out my thoughts merely to write a "Future directions" section.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#filling-in-the-sequence-end-api-table>Filling in the sequence-end API table

The gaps in the table suggest a number of APIs we could offer in the future. Here, I have filled in all options which are at least coherent:

Operand Get Index Exclude Remove (1) Pop (1) Equate (2)
Fixed Size
First 1 C.first C.firstIndex S.removingFirst() C.removeFirst() C.popFirst() -
Last 1 C.last C.lastIndex S.removingLast() C.removeLast() C.popLast() -
First (n: Int) S.prefix(_:slight_smile: C.prefixIndex(_:slight_smile: S.removingPrefix(_:slight_smile: C.removePrefix(_:slight_smile: - S.hasPrefix(_:slight_smile:
  ...with closure S.prefix(while:) C.prefixIndex(while:) S.removingPrefix(while:) C.removePrefix(while:) - S.hasPrefix(_:isEquivalent:)
Last (n: Int) S.suffix(_:slight_smile: C.suffixIndex(_:slight_smile: S.removingSuffix(_:slight_smile: C.removeSuffix(_:slight_smile: - S.hasSuffix(_:slight_smile:
  ...with closure S.suffix(while:) C.suffixIndex(while:) S.removingSuffix(while:) C.removeSuffix(while:) - S.hasSuffix(_:isEquivalent:)
Searching From End
First matching element S.earliest(_:slight_smile: C.earliestIndex(of:) S.removingEarliest(_:slight_smile: C.removeEarliest(_:slight_smile: - -
  ...with closure S.earliest(where:) C.earliestIndex(where:) S.removingEarliest(where:) C.removeEarliest(where:) - -
Last matching element S.latest(_:slight_smile: C.latestIndex(of:) S.removingLatest(_:slight_smile: C.removeLatest(_:slight_smile: - -
  ...with closure S.latest(where:) C.latestIndex(where:) S.removingLatest(where:) C.removeLatest(where:) - -
To explain a few entries which might not be immediately obvious: firstIndex and lastIndex would be nil if the collection is empty, and lastIndex would be the index before endIndex. prefixIndex would return the last index of the prefix, and suffixIndex would return the first index of the suffix; alternatively, these could be named with Indices and return ranges. earliest(_:slight_smile: and latest(_:slight_smile: would return the first and last element equal to the provided value; on a Set, they would be roughly equivalent to NSSet.member(_:).

The changes I consider most worthy include:

Adding corresponding last, suffix, and latest methods for all first, prefix, and earliest methods.

Adding corresponding while: versions of all appropriate prefix/suffix APIs.

Ones that could be useful, but can usually be emulated with more work:

Adding remove/removing-by-content APIs.

Adding prefix/suffixIndex(while:).

Ones that are mere conveniences or may not have strong use cases:

first/lastIndex and prefix/suffixIndex(_:).

earliest/latest(_:).

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#all-and-every-as-operands>"All" and "Every" as operands

One could imagine adding rows to this table for "all" and "every matching". In addition to creating some useful new API, this would also suggest some interesting renaming for existing APIs:

allIndices would be a name for indices.

removeAll() is actually an existing name which happens to fit this pattern.

every(where:) would be a name for filter. Though I believe filter is a strong term of art, I do note that every(where:) does not cause confusion about the sense of its test, a major complaint about filter.

In the table below, bold indicates new functionality; italics indicates existing functionality renamed to fit this pattern.

Operand Get Index Exclude Remove (1) Pop (1) Equate (2)
Fixed Size
First 1 C.first C.firstIndex S.removingFirst() C.removeFirst() C.popFirst() -
Last 1 C.last C.lastIndex S.removingLast() C.removeLast() C.popLast() -
First (n: Int) S.prefix(_:slight_smile: C.prefixIndex(_:slight_smile: S.removingPrefix(_:slight_smile: C.removePrefix(_:slight_smile: - S.hasPrefix(_:slight_smile:
  ...with closure S.prefix(while:) C.prefixIndex(while:) S.removingPrefix(while:) C.removePrefix(while:) - S.hasPrefix(_:isEquivalent:)
Last (n: Int) S.suffix(_:slight_smile: C.suffixIndex(_:slight_smile: S.removingSuffix(_:slight_smile: C.removeSuffix(_:slight_smile: - S.hasSuffix(_:slight_smile:
  ...with closure S.suffix(while:) C.suffixIndex(while:) S.removingSuffix(while:) C.removeSuffix(while:) - S.hasSuffix(_:isEquivalent:)
All - allIndices - C.removeAll() - -
Searching From End
First matching element S.earliest(_:slight_smile: C.earliestIndex(of:) S.removingEarliest(_:slight_smile: C.removeEarliest(_:slight_smile: - -
  ...with closure S.earliest(where:) C.earliestIndex(where:) S.removingEarliest(where:) C.removeEarliest(where:) - -
Last matching element S.latest(_:slight_smile: C.latestIndex(of:) S.removingLatest(_:slight_smile: C.removeLatest(_:slight_smile: - -
  ...with closure S.latest(where:) C.latestIndex(where:) S.removingLatest(where:) C.removeLatest(where:) - -
Every matching element S.every(_:slight_smile: C.everyIndex(of:) S.removingEvery(_:slight_smile: C.removeEvery(_:slight_smile: - -
  ...with closure S.every(where:) C.everyIndex(where:) S.removingEvery(where:) C.removeEvery(where:) - -
An alternative to the every methods is to give them names based on all or any, but these tend to require breaks from the naming patterns of the matching earliest and latest methods to remain grammatical.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#additional-index-based-operations>Additional index-based operations

Though accessing a range of elements bounded by the end of the collection is useful, it might be useful to extend that ability to other range-based collection APIs. IncompleteRange would make this especially easy; we would simply overload Range-taking APIs to permit IncompleteRanges as well. However, we could also provide variants of these APIs which take a to:, through:, or from: index parameter in place of an index range.

Candidates include:

MutableCollection.subscript(bounds: Range<Index>) { set }, making the subscripts in this proposal mutable.

RangeReplaceableCollection.removeSubrange(\_:slight_smile:

RangeReplaceableCollection.replaceSubrange(\_:with:)

The various Range parameters in String (although these might be better replaced with slice-based APIs).

···

--
Brent Royal-Gordon
Architechies


(Anton Zhilin) #2

• Renaming all methods which operate on more than one element at the
beginning/end to use "prefix" or "suffix", not "first" or "last"

+1

• Renaming `index(of:/where:)` to `earliestIndex(…)` and `first(where:)`
to `earliest(where:)`

-1, because `index` is considered state-of-art. `first` does not exist in
all languages, but `earliest` is used nowhere.

• Renaming the `drop` methods to use `removing`

+0.5, because drop functions came from functional languages, but
removingPrefix is much more to-the-point than dropFirst.

• Redesigning `prefix(upTo:)`, `prefix(through:)` and `suffix(from:)` as
subscripts with "partial" ranges, like `people[..<idx]` or perhaps
`people[nil..<idx]`.

-1, because prefix and suffix are state-of-art again, plus they match the
theme you are proposing, while subscripts do not.


(David Hart) #3

Most of your proposal look great to me! Comments inline:

As previously threatened mentioned, I've written a draft proposal to fix a number of naming issues with APIs operating on the beginning and end of Sequences and Collections:

• Inconsistent use of `prefix`/`suffix` vs. `first`/`last`
• Confusing naming of `drop` methods
• Ambiguous naming of `index(of:/where:)` and `drop(while:)`
• `prefix(upTo:)`, `prefix(through:)`, and `suffix(from:)` shouldn't be part of this family at all

To fix this, I propose:

• Renaming all methods which operate on more than one element at the beginning/end to use "prefix" or "suffix", not "first" or "last"
• Renaming `index(of:/where:)` to `earliestIndex(…)` and `first(where:)` to `earliest(where:)`
• Renaming the `drop` methods to use `removing`

+! Everything above, I strongly agree with!

• Redesigning `prefix(upTo:)`, `prefix(through:)` and `suffix(from:)` as subscripts with "partial" ranges, like `people[..<idx]` or perhaps `people[nil..<idx]`.

I’m not a fan of the subscript solutions. They both introduce new types which seems very heavyweight for such a small use case. I’d vote for keeping the current functions.

Since that last point requires significant redesign, including the introduction of new types, I have also included an alternative design which uses `people[to: idx]` instead.

This proposal does not seek to add new functionality; it merely renames or (in the case of the "aggressive" subscript option) redesigns existing functionality. I do, however, discuss (without making many judgements about their wisdom) how these changes might affect the naming of functionality we might add in future versions of Swift.

I would mainly like feedback on the two most open questions left in this proposal:

• The choice of `removing` to replace `drop`

Yep, heavily agree with you on `removing`.

• The decision about whether to use `people[..<idx]`, `people[nil..<idx]`, or `people[to: idx]`.

None of the above, as stated previously :slight_smile:

···

On 23 Jun 2016, at 09:19, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

But I'd also like comments on the rest of the proposal, and on whether I should split the prefix(upTo:/through:)/suffix(from:) changes into a separate proposal from the rest.

I suspect this will cause a firestorm of bikeshedding, so please try to keep your suggestions grounded. Don't just suggest a name; articulate why it's a better choice than what we already have or what this proposal suggests. Only you can prevent our first *three*-hundred-message bikeshedding thread.

Thanks for your attention!

(P.S. The proposal below includes several huge tables which may cause some mail clients to become very pouty and refuse to eat their supper. You may want to read the proposal at <https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430> instead.)

Rationalizing Sequence end-operation names
Proposal: SE-NNNN <https://gist.github.com/brentdax/NNNN-sequence-end-op.md>
Author: Brent Royal-Gordon <https://github.com/brentdax>
Status: Draft
Review manager: TBD
<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#introduction>Introduction

Sequence and Collection offer many special operations which access or manipulate its first or last elements, but they are plagued by inconsistent naming which can make it difficult to find inverses or remember what the standard library offers. I propose that we standardize these names so they follow consistent, predictable patterns.

Swift-evolution thread: Pending <http://news.gmane.org/gmane.comp.lang.swift.evolution>
<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#scope>Scope

This proposal is not intended to add or remove any functionality; it merely renames and redesigns existing operations. Adding new operations is out of scope for this proposal unless it's incidental to the new designs.

Nonetheless, I do want the new designs to support adding more operations in the future. The names I've chosen are informed by some of the speculative APIs discussed in "Future directions", although I think they are perfectly sensible names even if nothing else changes.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#motivation>Motivation

The Sequence and Collection protocols offer a wide variety of APIs which are defined to operate on, or from, one end of the sequence:

Operand Get Index Exclude Remove (1) Pop (1) Equate (2)
Fixed Size
First 1 C.first - S.dropFirst() C.removeFirst() C.popFirst() -
Last 1 C.last - S.dropLast() C.removeLast() C.popLast() -
First (n: Int) S.prefix(_:slight_smile: - S.dropFirst(_:slight_smile: C.removeFirst(_:slight_smile: - S.starts(with:)
  ...with closure S.prefix(while:) - S.drop(while:) - - S.starts(with:isEquivalent:)
Last (n: Int) S.suffix(_:slight_smile: - S.dropLast(_:slight_smile: C.removeLast(_:slight_smile: - -
  ...with closure - - - - - -
Searching From End
First matching element - C.index(of:) - - - -
  ...with closure S.first(where:) C.index(where:) - - - -
Last matching element - - - - - -
  ...with closure - - - - - -
Based on Index
startIndex ..< (i: Index) C.prefix(upTo:) - - - - -
startIndex ... (i: Index) C.prefix(through:) - - - - -
(i: Index) ..< endIndex C.suffix(from:) - - - - -
I have included several blank rows for operands which fit the APIs' patterns, even if they don't happen to have any operations currently.

Type abbreviations:

S = Sequence
C = Collection (or a sub-protocol like BidirectionalCollection)
Notes:

remove and pop both mutate the array to delete the indicated element(s), but remove assumes as a precondition that the indicated elements exist, while pop checks whether or not they exist.

String and NSString have bespoke versions of first n and last n Equate operations, in the form of their hasPrefix and hasSuffix methods.

Leaving aside the question of whether any gaps in these tables ought to be filled, I see a number of issues with existing terminology.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#inconsistent-use-of-prefix-and-suffix>Inconsistent use of prefix and suffix

Some APIs which operate on a variable number of elements anchored at one end or the other use the terms prefix or suffix:

Sequence.prefix(_:slight_smile: and Sequence.suffix(_:slight_smile:
Sequence.prefix(while:)
String.hasPrefix(_:slight_smile: and String.hasSuffix(_:slight_smile:
Others, however, use first or last:

Sequence.dropFirst(_:slight_smile: and Sequence.dropLast(_:slight_smile:
Sequence.removeFirst(_:slight_smile: and Sequence.removeLast(_:slight_smile:
Still others use neither:

Sequence.starts(with:)
Sequence.drop(while:)
These methods are all closely related, but because of this inconsistent terminology, they fail to form predictable method families.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#first-has-multiple-meanings>first has multiple meanings

The word first can mean three different things in these APIs:

Just the very first element of the sequence.

A subsequence of elements anchored at the beginning of the sequence, as mentioned in the last point.

The first element encountered in the sequence which matches a given criterion when walking from the beginning of the sequence towards the end.

It would be nice to have more clarity here.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#drop-is-misleading-and-scary>drop is misleading and scary

In a Swift context, I believe the drop methods are actively confusing:

drop does not have the -ing or -ed suffix normally used for a nonmutating method.

drop has strong associations with destructive operations; it's the term used, for instance, for deleting whole tables in SQL. Even dropping would probably sound more like a mutating operation than alternatives.

As previously mentioned, the use of dropFirst and dropLast for single-drop operations and multiple-drop operations breaks up method families.

drop, dropFirst, and dropLast are terms of art, so we allow them a certain amount of leeway. However, I believe the drop functions go well beyond what we should permit. They are relatively uncommon operations, associated primarily with functional languages rather than mainstream object-oriented or imperative languages, and their violation of the normal Swift naming guidelines is especially misleading.

The term-of-art exception is not a suicide pact; it is meant to aid understanding by importing common terminology, not bind us to follow every decision made by any language that came before us. In this case, I think we should ignore precedent and forge our own path.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#unstated-direction-of-operation>Unstated direction of operation

Several APIs could theoretically be implemented by working from either end of the sequence, and would return different results depending on the direction, but do not indicate the direction in their names:

Sequence.drop(while:)
Collection.index(of:)
Adding a direction to these APIs would make their behavior clearer and permit us to offer opposite-end equivalents in the future. (Unmerged swift-evolution pull request 329 <https://github.com/apple/swift-evolution/pull/329> would add lastIndex methods.)

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#the-index-base-name-has-been-polluted>The index(...) base name has been polluted

Swift 3's new collection model placed a number of low-level index manipulating operations on the base method name index. These now share that name with index(of:) and index(where:), which are much higher-level operations. This may be confusing for users looking for high-level operations; the only real relationship between the two sets of operations is that they both return an index.

It would be nice to separate these two groups of methods into different families.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#operations-taking-an-index-are-really-slicing>Operations taking an index are really slicing

prefix(upTo:), prefix(through:), and suffix(from:) at first appear to belong to the same family as the other prefix and suffix methods, but deeper examination reveals otherwise. They are the only operations which take indices, and they don't cleanly extend to the other operations which belong to these families. (For instance, it would not make sense to add a dropPrefix(upTo:) method; it would be equivalent to suffix(from:).)

Also, on Int-indexed collections like Array, prefix(_:slight_smile: and prefix(upTo:) are identical, but there is little relationship between suffix(_:slight_smile: and suffix(from:), which is confusing.

suffix(from:) is a particularly severe source of confusion. The other suffix APIs all have parameters relative to the endof the collection, but suffix(from:)'s index is still relative to the beginning of the array. This is obvious if you think deeply about the meaning of an index, but we don't really want to force our users to stare at a strange API until they have an epiphany.

I believe these operations have much more in common with slicing a collection using a range, and that reimagining them as slicing APIs will be more fruitful.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#why-does-it-matter>Why does it matter?

Many of these APIs are only occasionally necessary, so it's important that they be easy to find when needed and easy to understand when read. If you know that prefix(10) will get the first ten elements but don't know what its inverse is, you will probably not guess that it's dropFirst(10). The confusing, conflicting names in these APIs are a barrier to users adopting them where appropriate.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#proposed-solution>Proposed solution

We sever the index-taking APIs from the others, forming two separate families, which I will call the "Sequence-end operations" and the "index-based operations". We then consider and redesign them along separate lines.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#sequence-end-operations>Sequence-end operations

Each of these APIs should be renamed to use a directional word based on its row in the table:

Operand Directional word
Fixed Size
First 1 first
Last 1 last
First (n: Int) prefix
  ...with closure prefix
Last (n: Int) suffix
  ...with closure suffix
Searching From End
First matching element earliest
  ...with closure earliest
Last matching element latest
  ...with closure latest
To accomplish this, starts(with:) should be renamed to hasPrefix(_:), and other APIs should have directional words replaced or added as appropriate.

Additionally, the word drop in the "Exclude" APIs should be replaced with removing. These operations omit the same elements which the remove operations delete, so even though the types are not always the same (removing returns SubSequence, not Self), I think they are similar enough to deserve to be treated as nonmutating forms.

These changes yield (altered names bold):

Operand Get Index Exclude Remove (1) Pop (1) Equate (2)
Fixed Size
First 1 C.first - S.removingFirst() C.removeFirst() C.popFirst() -
Last 1 C.last - S.removingLast() C.removeLast() C.popLast() -
First (n: Int) S.prefix(_:slight_smile: - S.removingPrefix(_:slight_smile: C.removePrefix(_:slight_smile: - S.hasPrefix(_:slight_smile:
  ...with closure S.prefix(while:) - S.removingPrefix(while:) - - S.hasPrefix(_:isEquivalent:)
Last (n: Int) S.suffix(_:slight_smile: - S.removingSuffix(_:slight_smile: C.removeSuffix(_:slight_smile: - -
  ...with closure - - - - - -
Searching From End
First matching element - C.earliestIndex(of:) - - - -
  ...with closure S.earliest(where:) C.earliestIndex(where:) - - - -
Last matching element - - - - - -
  ...with closure - - - - - -
<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#alternative-to-removing>Alternative to removing

If the type differences are seen as disqualifying removing as a replacement for drop, I suggest using skipping instead.

There are, of course, many possible alternatives to skipping; this is almost a perfect subject for bikeshedding. I've chosen skipping because:

It is not an uncommon word, unlike (say) omitting. This means non-native English speakers and schoolchildren are more likely to recognize it.

It is an -ing verb, unlike (say) without. This makes it fit common Swift naming patterns more closely.

It does not imply danger, unlike (say) dropping, nor some sort of ongoing process, unlike (say) ignoring. This makes its behavior more obvious.

If you want to suggest an alternative on swift-evolution, please do not merely mention a synonym; rather, explain why it is an improvement on either these axes or other ones. (I would be particularly interested in names other than removing which draw an analogy to something else in Swift.)

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#index-based-operations>Index-based operations

Because these APIs look up elements based on their indices, I believe these operations should be exposed as subscripts, and ideally should look like other slicing operations.

My primary design is rather ambitious, introducing two new types and either two operator overloads, or four unary forms of existing binary operators. I therefore present a more conservative alternative as well.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#preferred-ambitious-option>Preferred (ambitious) option

let head = people[..<i]
let tail = people[i..<]
let rearrangedPeople = tail + head
Or this small variation:

let head = people[nil ..< i]
let tail = people[i ..< nil]
let rearrangedPeople = tail + head
The operators would construct instances of a new pair of types, IncompleteRange (for ..<) and IncompleteClosedRange (for ...), and Collection would include new subscripts taking these types. These would probably have default implementations which constructed an equivalent Range or ClosedRange using startIndex and endIndex, then passed the resulting range through to the existing subscripts.

I prefer this option because it offers an elegant syntax immediately recognizable as a form of slicing, and provides a straightforward way for a future version of Swift to extend other Range-handling Collection operations, like replaceSubrange(_:with:) and removeSubrange(_:), to handle subranges bound by the ends of the Collection.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#alternative-conservative-option>Alternative (conservative) option

let head = people[to: i]
let tail = people[from: i]
let rearrangedPeople = tail + head
This would be a simple changing of the methods into subscripts; no additional types or operators would be needed. I have changed upTo: into just to: to match the pattern set by stride(from:to:by:).

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#detailed-design>Detailed design

Draft note: This section is a mere sketch and should probably be expanded before submitting for a pull request.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#sequence-end-operations-1>Sequence-end operations

The following methods should be renamed as follows wherever they appear in the standard library. These are simple textual substitutions; we propose no changes whatsoever to types, parameter interpretations, or other semantics.

Old method New method
dropFirst() -> SubSequence removingFirst() -> SubSequence
dropLast() -> SubSequence removingLast() -> SubSequence
dropFirst(_ n: Int) -> SubSequence removingPrefix(_ n: Int) -> SubSequence
drop(@noescape while predicate: (Iterator.Element) throws -> Bool) rethrows -> SubSequence removingPrefix(@noescape while predicate: (Iterator.Element) throws -> Bool) rethrows -> SubSequence
dropLast(_ n: Int) -> SubSequence removingSuffix(_ n: Int) -> SubSequence
removeFirst(_ n: Int) removePrefix(_ n: Int)
removeLast(_ n: Int) removeSuffix(_ n: Int)
starts<PossiblePrefix: Sequence>(with possiblePrefix: PossiblePrefix) -> Bool where ... hasPrefix<PossiblePrefix: Sequence>(_ possiblePrefix: PossiblePrefix) -> Bool where ...
starts<PossiblePrefix : Sequence>(with possiblePrefix: PossiblePrefix, isEquivalent: @noescape (Iterator.Element, Iterator.Element) throws -> Bool) rethrows -> Bool where ... hasPrefix<PossiblePrefix : Sequence>(_ possiblePrefix: PossiblePrefix, isEquivalent: @noescape (Iterator.Element, Iterator.Element) throws -> Bool) rethrows -> Bool where ...
first(where predicate: @noescape (Iterator.Element) throws -> Bool) rethrows -> Iterator.Element? earliest(where predicate: @noescape (Iterator.Element) throws -> Bool) rethrows -> Iterator.Element?
index(of element: Iterator.Element) -> Index? earliestIndex(of element: Iterator.Element) -> Index?
index(where predicate: @noescape (Iterator.Element) throws -> Bool) rethrows -> Index? earliestIndex(where predicate: @noescape (Iterator.Element) throws -> Bool) rethrows -> Index?
<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#index-based-operations-1>Index-based operations

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#preferred-ambitious-option-1>Preferred (ambitious) option

Implementation would be roughly as follows:

Add a pair of new types, IncompleteRange and IncompleteClosedRange, with definitions like:

struct IncompleteRange<Bound: Comparable> {
   var lowerBound: Bound?
   var upperBound: Bound?

   func completed(with bounds: Range<Bound>) -> Range<Bound> {
       return (lowerBound ?? bounds.lowerBound) ..< (upperBound ?? bounds.upperBound)
   }
}
// And likewise for `IncompleteClosedRange`
Either add prefix and postfix ..< and ... operators, or overload the existing infix ..< and ... operators, to construct an IncompleteRange or IncompleteClosedRange. For the prefix/postfix operators, this would look like:

postfix func ..< <Bound: Comparable>(lowerBound: Bound) -> IncompleteRange<Bound> {
   return IncompleteRange(lowerBound: lowerBound, upperBound: nil)
}
prefix func ..< <Bound: Comparable>(upperBound: Bound) -> IncompleteRange<Bound> {
      return IncompleteRange(lowerBound: nil, upperBound: upperBound)
}
// And likewise for `...` and `IncompleteClosedRange`
Add a pair of IncompleteRange and IncompleteClosedRange subscripts to Collection:

extension Collection {
   subscript (range: IncompleteRange<Index>) -> SubSequence {
       return self[range.completed(with: startIndex ..< endIndex)]
   }
   // And likewise for `IncompleteClosedRange`
}
<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#alternative-conservative-option-1>Alternative (conservative) option

The three methods below will be replaced with equivalent subscripts in all types conforming to Collection or a sub-protocol:

Old method New subscript
prefix(upTo end: Index) -> SubSequence subscript(to end: Index) -> SubSequence
prefix(through position: Index) -> SubSequence subscript(through position: Index) -> SubSequence
func suffix(from start: Index) -> SubSequence subscript(from start: Index) -> SubSequence
<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#impact-on-existing-code>Impact on existing code

Obviously, any code using these APIs under their old names or designs would have to be transitioned to the new names and designs.

The sequence-end operations would be by far the simplest to handle; these are simple renamings and could be handed by @available(renamed:) and migration support. The only complication is that some overloads have transitioned to a new base name, while others have stayed with the old one, but I suspect the migrator is up to this task.

The preferred option for index-based operations is more difficult to migrate. The patterns would be roughly:

collection.prefix(upTo: i) => collection[..<i] or collection[nil ..< i]
collection.prefix(through: i) => collection[...i] or collection[nil ... i]
collection.suffix(from: i) => collection[i..<] or collection[i ..< nil]
A custom fix-it would be ideal, but is probably not absolutely necessary here; an @available(message:) would do in a pinch. Presumably this would have to be a special case in the migrator as well.

The alternative option for index-based operations would be simpler to fix-it and migrate—we need merely change references from the method to the subscript with the same (or similar, for upTo) parameter labels. I'm not sure if @available(renamed:) supports changing a method to a subscript.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#alternatives-considered>Alternatives considered

I considered many alternatives to removing or skipping, some of which are discussed in the section about them.

Rather than using earliest and latest, I considered using first and last for the "First matching" and "Last matching" categories. However, I didn't like the thought of overloading a property with a function, and I felt like earliest and latest actually have different semantics (searching for the match closest to a particular end) than first and last (manipulating the element at that end).

I considered using first and last as the basis for both single-element and multiple-element operations (such that prefix(3) would become first(3), etc.), but:

These seemed like distinct functionalities, particularly since their types are different.

I'm not comfortable with heavily overloading a property with a bunch of methods, and didn't want to make firstand last into methods.

Most APIs work fine, but hasFirst(_:slight_smile: is atrocious, and I see no better alternative which includes the word first.

I considered moving first and last to Sequence and possibly making them methods, but my understanding is that the core team has considered and rejected this approach in the past.

I considered moving removingFirst and removingLast to Collection and making them properties, to match firstand last, but this seemed like the sort of foolish consistency that Ralph Waldo Emerson warned of.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#future-directions>Future directions

Note: The rest of this proposal is highly speculative and there's probably no need to read further.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#other-sequence-api-cleanups>Other Sequence API cleanups

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#seriously-source-breaking>Seriously source-breaking

There is an ongoing discussion about which, if any, of map, flatMap, filter, and reduce ought to be renamed to more closely match Swift naming conventions. There is also discussion about relabeling many closure parameters.

The "Future directions" section below suggests every(where:) as an alternative to filter which could be extended in ways compatible with this proposal.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#significantly-source-breaking>Significantly source-breaking

The removeSubrange(_:slight_smile: and replaceSubrange(_:with:) APIs are rather oddly named. They might be better renamed to, for instance, remove(in:) and replace(in:with:).

It is not clear how important removingFirst() and removingLast() actually are, given that they're exactly equivalent to removingPrefix(1) and removingSuffix(1), and their corresponding "get" members are on Collection instead of Sequence. They could be removed.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#slightly-source-breaking>Slightly source-breaking

removeFirst/Last() and popFirst/Last() are very nearly redundant; their only difference is that the removemethods have a non-Optional return type and require the collection not be empty, while the pop methods have an Optional return type and return nil if it's empty.

These operations could be merged, with the remove operations taking on the preconditions of the current popoperations; additionally, removePrefix(_:slight_smile: and removeSuffix(_:slight_smile: could drop their equivalent preconditions requiring that the elements being removed exist. These changes would simplify the standard library and make these methods more closely parallel the equivalent removing methods, which do not have similar preconditions.

Performance-critical code which wants to avoid the checks necessary to remove these preconditions could switch to remove(at:) and removeSubrange(_:), which would continue to reject invalid indices.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#adding-sequence-and-collection-operations>Adding sequence and collection operations

This exercise in renaming suggests all sorts of other APIs we might add, and a few we might rename.

In general, I have not attempted to carefully scrutinize the usefulness of each of these APIs; instead, I have merely listed the ones which I can imagine some kind of use for. The main exception is the "Pop" operation; I can imagine several different, and rather incompatible, ways to extend it, and I'm not going to take the time to sort out my thoughts merely to write a "Future directions" section.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#filling-in-the-sequence-end-api-table>Filling in the sequence-end API table

The gaps in the table suggest a number of APIs we could offer in the future. Here, I have filled in all options which are at least coherent:

Operand Get Index Exclude Remove (1) Pop (1) Equate (2)
Fixed Size
First 1 C.first C.firstIndex S.removingFirst() C.removeFirst() C.popFirst() -
Last 1 C.last C.lastIndex S.removingLast() C.removeLast() C.popLast() -
First (n: Int) S.prefix(_:slight_smile: C.prefixIndex(_:slight_smile: S.removingPrefix(_:slight_smile: C.removePrefix(_:slight_smile: - S.hasPrefix(_:slight_smile:
  ...with closure S.prefix(while:) C.prefixIndex(while:) S.removingPrefix(while:) C.removePrefix(while:) - S.hasPrefix(_:isEquivalent:)
Last (n: Int) S.suffix(_:slight_smile: C.suffixIndex(_:slight_smile: S.removingSuffix(_:slight_smile: C.removeSuffix(_:slight_smile: - S.hasSuffix(_:slight_smile:
  ...with closure S.suffix(while:) C.suffixIndex(while:) S.removingSuffix(while:) C.removeSuffix(while:) - S.hasSuffix(_:isEquivalent:)
Searching From End
First matching element S.earliest(_:slight_smile: C.earliestIndex(of:) S.removingEarliest(_:slight_smile: C.removeEarliest(_:slight_smile: - -
  ...with closure S.earliest(where:) C.earliestIndex(where:) S.removingEarliest(where:) C.removeEarliest(where:) - -
Last matching element S.latest(_:slight_smile: C.latestIndex(of:) S.removingLatest(_:slight_smile: C.removeLatest(_:slight_smile: - -
  ...with closure S.latest(where:) C.latestIndex(where:) S.removingLatest(where:) C.removeLatest(where:) - -
To explain a few entries which might not be immediately obvious: firstIndex and lastIndex would be nil if the collection is empty, and lastIndex would be the index before endIndex. prefixIndex would return the last index of the prefix, and suffixIndex would return the first index of the suffix; alternatively, these could be named with Indices and return ranges. earliest(_:slight_smile: and latest(_:slight_smile: would return the first and last element equal to the provided value; on a Set, they would be roughly equivalent to NSSet.member(_:).

The changes I consider most worthy include:

Adding corresponding last, suffix, and latest methods for all first, prefix, and earliest methods.

Adding corresponding while: versions of all appropriate prefix/suffix APIs.

Ones that could be useful, but can usually be emulated with more work:

Adding remove/removing-by-content APIs.

Adding prefix/suffixIndex(while:).

Ones that are mere conveniences or may not have strong use cases:

first/lastIndex and prefix/suffixIndex(_:).

earliest/latest(_:).

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#all-and-every-as-operands>"All" and "Every" as operands

One could imagine adding rows to this table for "all" and "every matching". In addition to creating some useful new API, this would also suggest some interesting renaming for existing APIs:

allIndices would be a name for indices.

removeAll() is actually an existing name which happens to fit this pattern.

every(where:) would be a name for filter. Though I believe filter is a strong term of art, I do note that every(where:) does not cause confusion about the sense of its test, a major complaint about filter.

In the table below, bold indicates new functionality; italics indicates existing functionality renamed to fit this pattern.

Operand Get Index Exclude Remove (1) Pop (1) Equate (2)
Fixed Size
First 1 C.first C.firstIndex S.removingFirst() C.removeFirst() C.popFirst() -
Last 1 C.last C.lastIndex S.removingLast() C.removeLast() C.popLast() -
First (n: Int) S.prefix(_:slight_smile: C.prefixIndex(_:slight_smile: S.removingPrefix(_:slight_smile: C.removePrefix(_:slight_smile: - S.hasPrefix(_:slight_smile:
  ...with closure S.prefix(while:) C.prefixIndex(while:) S.removingPrefix(while:) C.removePrefix(while:) - S.hasPrefix(_:isEquivalent:)
Last (n: Int) S.suffix(_:slight_smile: C.suffixIndex(_:slight_smile: S.removingSuffix(_:slight_smile: C.removeSuffix(_:slight_smile: - S.hasSuffix(_:slight_smile:
  ...with closure S.suffix(while:) C.suffixIndex(while:) S.removingSuffix(while:) C.removeSuffix(while:) - S.hasSuffix(_:isEquivalent:)
All - allIndices - C.removeAll() - -
Searching From End
First matching element S.earliest(_:slight_smile: C.earliestIndex(of:) S.removingEarliest(_:slight_smile: C.removeEarliest(_:slight_smile: - -
  ...with closure S.earliest(where:) C.earliestIndex(where:) S.removingEarliest(where:) C.removeEarliest(where:) - -
Last matching element S.latest(_:slight_smile: C.latestIndex(of:) S.removingLatest(_:slight_smile: C.removeLatest(_:slight_smile: - -
  ...with closure S.latest(where:) C.latestIndex(where:) S.removingLatest(where:) C.removeLatest(where:) - -
Every matching element S.every(_:slight_smile: C.everyIndex(of:) S.removingEvery(_:slight_smile: C.removeEvery(_:slight_smile: - -
  ...with closure S.every(where:) C.everyIndex(where:) S.removingEvery(where:) C.removeEvery(where:) - -
An alternative to the every methods is to give them names based on all or any, but these tend to require breaks from the naming patterns of the matching earliest and latest methods to remain grammatical.

<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430#additional-index-based-operations>Additional index-based operations

Though accessing a range of elements bounded by the end of the collection is useful, it might be useful to extend that ability to other range-based collection APIs. IncompleteRange would make this especially easy; we would simply overload Range-taking APIs to permit IncompleteRanges as well. However, we could also provide variants of these APIs which take a to:, through:, or from: index parameter in place of an index range.

Candidates include:

MutableCollection.subscript(bounds: Range<Index>) { set }, making the subscripts in this proposal mutable.

RangeReplaceableCollection.removeSubrange(\_:slight_smile:

RangeReplaceableCollection.replaceSubrange(\_:with:)

The various Range parameters in String (although these might be better replaced with slice-based APIs).

--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(David Hart) #4

Most of your proposal look great to me! Comments inline:

As previously threatened mentioned, I've written a draft proposal to fix a number of naming issues with APIs operating on the beginning and end of Sequences and Collections:

• Inconsistent use of `prefix`/`suffix` vs. `first`/`last`
• Confusing naming of `drop` methods
• Ambiguous naming of `index(of:/where:)` and `drop(while:)`
• `prefix(upTo:)`, `prefix(through:)`, and `suffix(from:)` shouldn't be part of this family at all

To fix this, I propose:

• Renaming all methods which operate on more than one element at the beginning/end to use "prefix" or "suffix", not "first" or "last"
• Renaming `index(of:/where:)` to `earliestIndex(…)` and `first(where:)` to `earliest(where:)`
• Renaming the `drop` methods to use `removing`

+! Everything above, I strongly agree with!

• Redesigning `prefix(upTo:)`, `prefix(through:)` and `suffix(from:)` as subscripts with "partial" ranges, like `people[..<idx]` or perhaps `people[nil..<idx]`.

I’m not a fan of the subscript solutions. They both introduce new types which seems very heavyweight for such a small use case. I’d vote for keeping the current functions.

Since that last point requires significant redesign, including the introduction of new types, I have also included an alternative design which uses `people[to: idx]` instead.

This proposal does not seek to add new functionality; it merely renames or (in the case of the "aggressive" subscript option) redesigns existing functionality. I do, however, discuss (without making many judgements about their wisdom) how these changes might affect the naming of functionality we might add in future versions of Swift.

I would mainly like feedback on the two most open questions left in this proposal:

• The choice of `removing` to replace `drop`

Yep, heavily agree with you on `removing`.

• The decision about whether to use `people[..<idx]`, `people[nil..<idx]`, or `people[to: idx]`.

None of the above, as stated previously :slight_smile:

···

On 23 Jun 2016, at 09:19, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

But I'd also like comments on the rest of the proposal, and on whether I should split the prefix(upTo:/through:)/suffix(from:) changes into a separate proposal from the rest.

I suspect this will cause a firestorm of bikeshedding, so please try to keep your suggestions grounded. Don't just suggest a name; articulate why it's a better choice than what we already have or what this proposal suggests. Only you can prevent our first *three*-hundred-message bikeshedding thread.

Thanks for your attention!

(P.S. The proposal below includes several huge tables which may cause some mail clients to become very pouty and refuse to eat their supper. You may want to read the proposal at <https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430> instead.)


(Guillaume Lessard) #5

I like this draft. Quickly:

- “removing” sounds more destructive than “skipping”, so I’d lean towards using “skipping”. I would be happy with either, though.
- there are two different labels for predicates (where and while). “where” is probably the better label. (some previous discussion had seemingly arrived to that conclusion: http://article.gmane.org/gmane.comp.lang.swift.evolution/16334/)

I like the idea of making the index-based slicing operations look more like slicing operations. I have on occasion wanted the operators you describe in the “aggressive” option; I approve.

Cheers,
Guillaume Lessard


(Anders) #6

As previously threatened mentioned, I've written a draft proposal to fix a number of naming issues with APIs operating on the beginning and end of Sequences and Collections:

• Inconsistent use of `prefix`/`suffix` vs. `first`/`last`
• Confusing naming of `drop` methods
• Ambiguous naming of `index(of:/where:)` and `drop(while:)`
• `prefix(upTo:)`, `prefix(through:)`, and `suffix(from:)` shouldn't be part of this family at all

To fix this, I propose:

• Renaming all methods which operate on more than one element at the beginning/end to use "prefix" or "suffix", not "first" or "last"

Just want to point out that `prefix` and `suffix` may introduce ambiguity, as they are also imperative verbs. For example, `S.prefix(1)` can be inferred as prefixing `1` to the sequence, instead of retrieving the prefix of the sequence.

IMO `skip`, as an alternative listed in the proposal, would convey the intention better and clearer, and it has roots in the sequence-like constructs of reactive programming libraries (e.g. RxSwift and ReactiveCocoa).

···

On 23 Jun 2016, at 3:19 PM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

• Renaming `index(of:/where:)` to `earliestIndex(…)` and `first(where:)` to `earliest(where:)`
• Renaming the `drop` methods to use `removing`
• Redesigning `prefix(upTo:)`, `prefix(through:)` and `suffix(from:)` as subscripts with "partial" ranges, like `people[..<idx]` or perhaps `people[nil..<idx]`.

Since that last point requires significant redesign, including the introduction of new types, I have also included an alternative design which uses `people[to: idx]` instead.

This proposal does not seek to add new functionality; it merely renames or (in the case of the "aggressive" subscript option) redesigns existing functionality. I do, however, discuss (without making many judgements about their wisdom) how these changes might affect the naming of functionality we might add in future versions of Swift.

I would mainly like feedback on the two most open questions left in this proposal:

• The choice of `removing` to replace `drop`
• The decision about whether to use `people[..<idx]`, `people[nil..<idx]`, or `people[to: idx]`.

But I'd also like comments on the rest of the proposal, and on whether I should split the prefix(upTo:/through:)/suffix(from:) changes into a separate proposal from the rest.

I suspect this will cause a firestorm of bikeshedding, so please try to keep your suggestions grounded. Don't just suggest a name; articulate why it's a better choice than what we already have or what this proposal suggests. Only you can prevent our first *three*-hundred-message bikeshedding thread.

Thanks for your attention!

(P.S. The proposal below includes several huge tables which may cause some mail clients to become very pouty and refuse to eat their supper. You may want to read the proposal at <https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430> instead.)
--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brent Royal-Gordon) #7

I apologize for not getting the threading right in this email, Anton; Mail seems to have glitched and lost your message.

• Renaming `index(of:/where:)` to `earliestIndex(…)` and `first(where:)`
to `earliest(where:)`

-1, because `index` is considered state-of-art. `first` does not exist in
all languages, but `earliest` is used nowhere.

• Redesigning `prefix(upTo:)`, `prefix(through:)` and `suffix(from:)` as
subscripts with "partial" ranges, like `people[..<idx]` or perhaps
`people[nil..<idx]`.

-1, because prefix and suffix are state-of-art again, plus they match the
theme you are proposing, while subscripts do not.

I know that there are terms of art in this area, but I am explicitly ignoring them. This paragraph appears in the `drop` section, but it really applies to the entire proposal:

The term-of-art exception is not a suicide pact; it is meant to aid understanding by importing common terminology, not bind us to follow every decision made by any language that came before us. In this case, I think we should ignore precedent and forge our own path.

Almost every change in this proposal violates the term-of-art rule. I'm proposing them anyway because, in my opinion, we have too closely followed many weak terms of art in an area which would benefit more from a design with consistent, simple rules.

I will address the prefix/suffix thing in a joint reply to you and several others, but your complaints about `index` are, I believe, unique.

`index` is not a strong term of art; in my survey of a few languages that , languages seem to use "find", "index", or both, but in a number of different ways. In alphabetical order by function/method name:

• Haskell: `elemIndex`
• C++: `find`
• Python: `find` and `index` (the former returns a sentinel value on error; the latter throws)
• Javascript: `findIndex`
• Ruby: `find_index`
• Perl: `first_index` (in List::MoreUtils)
• Objective-C: `indexOf:`
• Java: `indexOf`
• Scala: `indexOf`
• C#: `IndexOf`

Moreover, <https://github.com/apple/swift-evolution/pull/329> presents a compelling reason to add a direction to these methods: so we can provide a matching set of index-finding methods from the end of the collection. Languages in this list which offer one usually end up creating two asymmetrically-named methods, like `indexOf` and `lastIndexOf`. I don't think that's a good solution.

You are quite right that `earliest` is a novel invention, unprecedented in other languages as far as I know. I think there's value in distinguishing between the "first" operations (which operate on the sequence's very first element without doing any searching) and the "earliest" operations (which perform a usually linear search from the beginning of the collection to the end). I also think that, if you look at the tables in "Adding sequence and collection operations", some of the potential methods in that table might be confusing if the "earliest" operations instead use "first". For instance, `removeFirst()` would remove the very first element, while `removeFirst(_:)` would search for a matching element and remove it.

Nevertheless, it *is* possible to use `first` instead of `earliest`, and in fact my decision to post the version using `earliest` was a last-minute one. I think `earliest` is a cleaner solution, but `first` is a reasonable alternative.

···

--
Brent Royal-Gordon
Architechies


(Dave Abrahams) #8

As previously threatened mentioned, I've written a draft proposal to
fix a number of naming issues with APIs operating on the beginning and
end of Sequences and Collections:

• Inconsistent use of `prefix`/`suffix` vs. `first`/`last`
• Confusing naming of `drop` methods
• Ambiguous naming of `index(of:/where:)` and `drop(while:)`
• `prefix(upTo:)`, `prefix(through:)`, and `suffix(from:)` shouldn't
be part of this family at all

To fix this, I propose:

• Renaming all methods which operate on more than one element at the
beginning/end to use "prefix" or "suffix", not "first" or "last"
• Renaming `index(of:/where:)` to `earliestIndex(…)` and
`first(where:)` to `earliest(where:)`

What's wrong with firstIndex(of:/where:) [and lastIndex(of:/where:)]?
That seems like a much less esoteric way to phrase it that meshes well
with the meanings of

     xs.first
     xs.indices.first

etc.

• Renaming the `drop` methods to use `removing`

Very clever! I *like*.

• Redesigning `prefix(upTo:)`, `prefix(through:)` and `suffix(from:)`
as subscripts with "partial" ranges, like `people[..<idx]` or perhaps
`people[nil..<idx]`.

Yes please; I really want this. This part is a slightly nontrivial
design problem, though. Someone should build an implementation before
the actual design is proposed. Probably the best way would be to
leave prefix and suffix alone for the moment and add/test the new
subscripts.

Since that last point requires significant redesign, including the
introduction of new types, I have also included an alternative design
which uses `people[to: idx]` instead.

I really don't like using labels for this, because stride(to:) and
stride(through:) have already spawned a naming bikeshed with no clear
resolution, suggesting that no name works. Plus, the ..< operator
already implies the name.

This proposal does not seek to add new functionality; it merely
renames or (in the case of the "aggressive" subscript option)
redesigns existing functionality. I do, however, discuss (without
making many judgements about their wisdom) how these changes might
affect the naming of functionality we might add in future versions of
Swift.

Good.

I would mainly like feedback on the two most open questions left in
this proposal:

• The choice of `removing` to replace `drop`

It's 100% appropriate, provided that the APIs match some corresponding
mutating remove API. Nonmutating operations are often implemented via
lazy adaptors... which a slice can be viewed to be. So I think this is
a beautiful answer.

• The decision about whether to use `people[..<idx]`,
`people[nil..<idx]`, or `people[to: idx]`.

I prefer how the first one reads.

But I'd also like comments on the rest of the proposal, and on whether
I should split the prefix(upTo:/through:)/suffix(from:) changes into a
separate proposal from the rest.

I very much appreciate that you're addressing all of these at once.

I suspect this will cause a firestorm of bikeshedding, so please try
to keep your suggestions grounded. Don't just suggest a name;
articulate why it's a better choice than what we already have or what
this proposal suggests. Only you can prevent our first
*three*-hundred-message bikeshedding thread.

Thanks for your attention!

(P.S. The proposal below includes several huge tables which may cause
some mail clients to become very pouty and refuse to eat their
supper. You may want to read the proposal at
<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430
<https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430>>
instead.)

The Sequence and Collection protocols offer a wide variety of APIs which
are defined to operate on, or from, one end of the sequence:

Operand Get Index Exclude Remove (1) Pop (1) Equate (2)

I think you want “Operation” or “Semantics” rather than “Operand” (which
means an argument to an operation)

Fixed Size
First 1 C.first - S.dropFirst() C.removeFirst() C.popFirst() -
Last 1 C.last - S.dropLast() C.removeLast() C.popLast() -
First (n: Int) S.prefix(_:slight_smile: - S.dropFirst(_:slight_smile: C.removeFirst(_:slight_smile: - S.starts(with:)
...with closure S.prefix(while:) - S.drop(while:) - - S.starts
      (with:isEquivalent:)
Last (n: Int) S.suffix(_:slight_smile: - S.dropLast(_:slight_smile: C.removeLast(_:slight_smile: - -
...with closure - - - - - -
Searching From End
First matching - C.index(of:) - - - -
element
...with closure S.first(where:) C.index(where:) - - - -
Last matching element - - - - - -
...with closure - - - - - -
Based on Index
startIndex ..< (i: Index) C.prefix(upTo:) - - - - -
startIndex ... (i: Index) C.prefix(through:) - - - - -
(i: Index) ..< endIndex C.suffix(from:) - - - - -

I have included several blank rows for operands which fit the APIs' patterns, even if they don't happen to have any operations currently.

Type abbreviations:

* S = Sequence
* C = Collection (or a sub-protocol like BidirectionalCollection)

Notes:

1 remove and pop both mutate the array to delete the indicated element(s), but remove assumes as a precondition that the indicated elements exist, while pop
checks whether or not they exist.

2 String and NSString have bespoke versions of first n and last n Equate operations, in the form of their hasPrefix and hasSuffix methods.

Leaving aside the question of whether any gaps in these tables ought to be filled, I see a number of issues with existing terminology.

SVG ImageInconsistent use of prefix and suffix

Some APIs which operate on a variable number of elements anchored at one end or the other use the terms prefix or suffix:

* Sequence.prefix(_:slight_smile: and Sequence.suffix(_:slight_smile:
* Sequence.prefix(while:)
* String.hasPrefix(_:slight_smile: and String.hasSuffix(_:slight_smile:

Others, however, use first or last:

* Sequence.dropFirst(_:slight_smile: and Sequence.dropLast(_:slight_smile:
* Sequence.removeFirst(_:slight_smile: and Sequence.removeLast(_:slight_smile:

Still others use neither:

* Sequence.starts(with:)
* Sequence.drop(while:)

These methods are all closely related, but because of this inconsistent terminology, they fail to form predictable method families.

SVG Imagefirst has multiple meanings

The word first can mean three different things in these APIs:

* Just the very first element of the sequence.

* A subsequence of elements anchored at the beginning of the sequence,
  as mentioned in the last point.

* The first element encountered in the sequence which matches a given
  criterion when walking from the beginning of the sequence towards the
  end.

It would be nice to have more clarity here.

You seem to be suggesting that a word needs to mean exactly the same
thing regardless of context. If so, I disagree. If I say “the first
element” or “the first element greater than 5” there's absolutely no
lack of clarity AFAICT. That accounts for the first and last bullets

The usage in the middle bullet is open to misinterpretation and I would
support fixing that.

     xs.removeFirst(42)

could read like, “remove the first element equal to 42.”

SVG Imagedrop is misleading and scary

In a Swift context, I believe the drop methods are actively confusing:

* drop does not have the -ing or -ed suffix normally used for a
nonmutating method.

* drop has strong associations with destructive operations; it's the
term used, for instance, for deleting whole tables in SQL. Even
dropping would probably sound more like a mutating operation than
alternatives.

* As previously mentioned, the use of dropFirst and dropLast for
single-drop operations and multiple-drop operations breaks up method
families.

drop, dropFirst, and dropLast are terms of art, so we allow them a
certain amount of leeway. However, I believe the drop functions go
well beyond what we should
permit. They are relatively uncommon operations, associated primarily
with functional languages rather than mainstream object-oriented or
imperative languages, and
their violation of the normal Swift naming guidelines is especially
misleading.

The term-of-art exception is not a suicide pact;

Tatoo that on your forehead, mister!

it is meant to aid understanding by importing common terminology, not
bind us to follow every decision made by any language that came before
us. In this case, I think we should ignore precedent and forge our own
path.

SVG ImageUnstated direction of operation

Several APIs could theoretically be implemented by working from either
end of the sequence, and would return different results depending on
the direction, but do not indicate the direction in their names:

* Sequence.drop(while:)
* Collection.index(of:)

Adding a direction to these APIs would make their behavior clearer and permit us to offer opposite-end equivalents in the future. (Unmerged swift-evolution pull
request 329 would add lastIndex methods.)

SVG ImageThe index(...) base name has been polluted

Swift 3's new collection model placed a number of low-level index
manipulating operations on the base method name index. These now share
that name with index(of:) and index(where:), which are much
higher-level operations. This may be confusing for users looking for
high-level operations; the only real relationship between the two sets
of operations is that they both return an index.

There's another relationship. Once you call the high-level operation,
you're now in the domain of indexing, and are very likely to ask for the
index(after:) the one you found.

It would be nice to separate these two groups of methods into
different families.

I used to think that was important, but I no longer do given the above.

SVG ImageOperations taking an index are really slicing

prefix(upTo:), prefix(through:), and suffix(from:) at first appear to
belong to the same family as the other prefix and suffix methods, but
deeper examination reveals otherwise. They are the only operations
which take indices, and they don't cleanly extend to the other
operations which belong to these families. (For instance, it would not
make sense to add a dropPrefix(upTo:) method; it would be equivalent
to suffix(from:).)

Also, on Int-indexed collections like Array, prefix(_:slight_smile: and
prefix(upTo:) are identical, but there is little relationship between
suffix(_:slight_smile: and suffix(from:), which is confusing.

suffix(from:) is a particularly severe source of confusion. The other
suffix APIs all have parameters relative to the endof the collection,
but suffix(from:)'s index is still relative to the beginning of the
array. This is obvious if you think deeply about the meaning of an
index, but we don't really want to force our users to stare at a
strange API until they have an epiphany.

I believe these operations have much more in common with slicing a
collection using a range, and that reimagining them as slicing APIs
will be more fruitful.

Yes please.

SVG ImageWhy does it matter?

Many of these APIs are only occasionally necessary, so it's important
that they be easy to find when needed and easy to understand when
read. If you know that prefix (10) will get the first ten elements but
don't know what its inverse is, you will probably not guess that it's
dropFirst(10). The confusing, conflicting names in these APIs are a
barrier to users adopting them where appropriate.

SVG ImageProposed solution

We sever the index-taking APIs from the others, forming two separate
families, which I will call the "Sequence-end operations" and the
"index-based operations". We then consider and redesign them along
separate lines.

SVG ImageSequence-end operations

Each of these APIs should be renamed to use a directional word based
on its row in the table:

Operand Directional word
Fixed Size
First 1 first
Last 1 last
First (n: Int) prefix
...with closure prefix
Last (n: Int) suffix
...with closure suffix
Searching From End
First matching element earliest
...with closure earliest
Last matching element latest
...with closure latest

To accomplish this, starts(with:) should be renamed to hasPrefix(_:),

+1

and other APIs should have directional words replaced or added as
appropriate.

Additionally, the word drop in the "Exclude" APIs should be replaced
with removing. These operations omit the same elements which the
remove operations delete, so even though the types are not always the
same (removing returns SubSequence, not Self), I think they are
similar enough to deserve to be treated as nonmutating forms.

These changes yield (altered names bold):

Operand Get Index Exclude Remove (1) Pop (1) Equate (2)
Fixed Size
First 1 C.first - S.removingFirst() C.removeFirst() C.popFirst() -
Last 1 C.last - S.removingLast() C.removeLast() C.popLast() -
First (n: Int) S.prefix(_:slight_smile: - S.removingPrefix(_:slight_smile: C.removePrefix(_:slight_smile: - S.hasPrefix(_:slight_smile:
...with closure S.prefix(while:) - S.removingPrefix - - S.hasPrefix
   (while:) (_:isEquivalent:)

Call me overly fussy, but I don't love the use of “while” here because
it seems stateful.

   xs.prefix(while: isFull)

That reads like I'm going to repeatedly take the prefix of xs while some
isFull property is true. The most descriptive usage I can think of is

   for x in xs.longestPrefix(where: isFull)

What do you think?

[BTW, you might need to stop using a table because it's already too
wide, but your examples *really* ought to be showing use cases rather
than signatures, c.f. the table in
https://github.com/apple/swift/pull/2981. Otherwise it's hard]

Last (n: Int) S.suffix(_:slight_smile: - S.removingSuffix(_:slight_smile: C.removeSuffix(_:slight_smile: -
- ...with closure - - - - - - Searching From End First matching -
C.earliestIndex(of:) - - - - element ...with closure
S.earliest(where:) C.earliestIndex - - - - (where:) Last matching
element - - - - - - ...with closure - - - - - -

SVG ImageAlternative to removing

If the type differences are seen as disqualifying removing as a
replacement for drop,

They are not!

I suggest using skipping instead.

There are, of course, many possible alternatives to skipping; this is
almost a perfect subject for bikeshedding. I've chosen skipping
because:

1 It is not an uncommon word, unlike (say) omitting. This means
non-native English speakers and schoolchildren are more likely to
recognize it.

2 It is an -ing verb, unlike (say) without. This makes it fit common
Swift naming patterns more closely.

3 It does not imply danger, unlike (say) dropping, nor some sort of
ongoing process, unlike (say) ignoring. This makes its behavior more
obvious.

If you want to suggest an alternative on swift-evolution, please do
not merely mention a synonym; rather, explain why it is an improvement
on either these axes or other ones. (I would be particularly
interested in names other than removing which draw an analogy to
something else in Swift.)

SVG ImageIndex-based operations

Because these APIs look up elements based on their indices, I believe these operations should be exposed as subscripts, and ideally should look like other slicing
operations.

My primary design is rather ambitious, introducing two new types and either two operator overloads, or four unary forms of existing binary operators. I therefore
present a more conservative alternative as well.

SVG ImagePreferred (ambitious) option

let head = people[..<i]
let tail = people[i..<]

let equivalentTail = people[i...] // reads a bit better, no?
let headThroughI = people[...i]

let rearrangedPeople = tail + head

Or this small variation:

let head = people[nil ..< i]
let tail = people[i ..< nil]
let rearrangedPeople = tail + head

The operators would construct instances of a new pair of types,
IncompleteRange (for ..<) and IncompleteClosedRange (for ...), and
Collection would include new subscripts taking these types. These
would probably have default implementations which constructed an
equivalent Range or ClosedRange using startIndex and endIndex, then
passed the resulting range through to the existing subscripts.

W00t!

···

on Thu Jun 23 2016, Brent Royal-Gordon <swift-evolution@swift.org> wrote:

I prefer this option because it offers an elegant syntax immediately
recognizable as a form of slicing, and provides a straightforward way
for a future version of Swift to extend other Range-handling
Collection operations, like replaceSubrange(_:with:) and
removeSubrange(_:), to handle subranges bound by the ends of the
Collection.

--
Dave


(Xiaodi Wu) #9

I like this draft. Quickly:

- “removing” sounds more destructive than “skipping”, so I’d lean towards
using “skipping”. I would be happy with either, though.
- there are two different labels for predicates (where and while). “where”
is probably the better label. (some previous discussion had seemingly
arrived to that conclusion:
http://article.gmane.org/gmane.comp.lang.swift.evolution/16334/)

I think this is going to the distinction that "where" is now supposed to
correspond to "continue" inside a loop if the predicate isn't satisfied,
whereas "while" is supposed to correspond to "break" inside a loop if the
predicate isn't satisfied. Then again, I've argued elsewhere on this list
that the distinction isn't nearly as obvious as some seem to think it is,
and I think we have another demonstration of that here :slight_smile:

···

On Fri, Jun 24, 2016 at 12:45 AM, Guillaume Lessard via swift-evolution < swift-evolution@swift.org> wrote:

I like the idea of making the index-based slicing operations look more
like slicing operations. I have on occasion wanted the operators you
describe in the “aggressive” option; I approve.

Cheers,
Guillaume Lessard

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Xiaodi Wu) #10

Yikes, not only is the email too big for some mail clients, it's too big
for the mailing list. Resending with proposal snipped.

Most of your proposal look great to me! Comments inline:

As previously threatened mentioned, I've written a draft proposal to fix
a number of naming issues with APIs operating on the beginning and end of
Sequences and Collections:

• Inconsistent use of `prefix`/`suffix` vs. `first`/`last`
• Confusing naming of `drop` methods
• Ambiguous naming of `index(of:/where:)` and `drop(while:)`
• `prefix(upTo:)`, `prefix(through:)`, and `suffix(from:)` shouldn't be
part of this family at all

To fix this, I propose:

• Renaming all methods which operate on more than one element at the
beginning/end to use "prefix" or "suffix", not "first" or "last"

Looking at the first column in your table, I think the current API focuses

correctly on the number of elements returned (and consequently, the return
type) rather than the number of elements interrogated. "First" and "last"
on their own suggest very strongly that you get back either zero elements
or one element (thus, an optional would be the appropriate return type),
whereas "prefix" and "suffix" suggest 0 to all elements might be returned
(thus, an array or collection would be the appropriate return type).
Currently, the API adheres to that expectation as far as your "get" column
is concerned, and IMO the most consistent approach would be to whip the
remaining columns in line.

To me, `first(where:)` is unambiguous and doubly distinguished from
`prefix(while:)`--even when the argument label is dropped in trailing
closure syntax, it is clear that `first` gives you at most one, and by
contradistinction `prefix` gives you at most all. I'm not sure that
"earliest" is an improvement, since you're introducing another word and
breaking the parallels here. So on reflection, I'm satisfied that first,
prefix, last, suffix all have their place in the API. Of course, I think
you could make a case for an across-the-board renaming of "first" to
"earliest" and "last" to "latest"--in fact, there could be an argument that
given 0-based indices the word "first" is an unfortunate choice in any case.

• Renaming `index(of:/where:)` to `earliestIndex(…)` and `first(where:)`
to `earliest(where:)`
• Renaming the `drop` methods to use `removing`

+! Everything above, I strongly agree with!

• Redesigning `prefix(upTo:)`, `prefix(through:)` and `suffix(from:)` as
subscripts with "partial" ranges, like `people[..<idx]` or perhaps
`people[nil..<idx]`.

I’m not a fan of the subscript solutions. They both introduce new types
which seems very heavyweight for such a small use case. I’d vote for
keeping the current functions.

Ditto, not a fan of a subscript solution here. It is true that
`suffix(from:)` seems weird at first, but it is literally nonsensical to
interpret the single argument any other way, partly because of the
mandatory argument label but mostly because a "suffix" that doesn't go to
the end isn't a suffix.

(Personally, though, I always thought `upTo` was an ineffective attempt to
clarify the distinction with `through`, since either you were familiar with
`stride(from:to:by:)` and `stride(from:through:by:)` or you weren't, and if
you weren't `upTo` is just as opaque as `to`.)

Since that last point requires significant redesign, including the
introduction of new types, I have also included an alternative design which
uses `people[to: idx]` instead.

This proposal does not seek to add new functionality; it merely renames or
(in the case of the "aggressive" subscript option) redesigns existing
functionality. I do, however, discuss (without making many judgements about
their wisdom) how these changes might affect the naming of functionality we
might add in future versions of Swift.

I would mainly like feedback on the two most open questions left in this
proposal:

• The choice of `removing` to replace `drop`

Yep, heavily agree with you on `removing`.

I think `removing` is an improvement. That said, your proposal raises a
good point that the return type goes against expectations for an `ed/ing`
counterpart. If you're looking for an alternative name, there's a pretty
good one at the top of your very nice table: `excluding`. It's a commonly
used English word, conveys exactly what's going on, and isn't scary and
verb-sounding like `drop`.

• The decision about whether to use `people[..<idx]`, `people[nil..<idx]`,
or `people[to: idx]`.

None of the above, as stated previously :slight_smile:

Yeah, not so much of a fan here either :confused:

···

On Thu, Jun 23, 2016 at 1:24 PM, David Hart via swift-evolution < swift-evolution@swift.org> wrote:

On 23 Jun 2016, at 09:19, Brent Royal-Gordon via swift-evolution < > swift-evolution@swift.org> wrote:

But I'd also like comments on the rest of the proposal, and on whether I
should split the prefix(upTo:/through:)/suffix(from:) changes into a
separate proposal from the rest.

I suspect this will cause a firestorm of bikeshedding, so please try to
keep your suggestions grounded. Don't just suggest a name; articulate why
it's a better choice than what we already have or what this proposal
suggests. Only you can prevent our first *three*-hundred-message
bikeshedding thread.

Thanks for your attention!

(P.S. The proposal below includes several huge tables which may cause some
mail clients to become very pouty and refuse to eat their supper. You may
want to read the proposal at <
https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430>
instead.)


(Brent Royal-Gordon) #11

Just want to point out that `prefix` and `suffix` may introduce ambiguity, as they are also imperative verbs. For example, `S.prefix(1)` can be inferred as prefixing `1` to the sequence, instead of retrieving the prefix of the sequence.

While that is true, I think the types will usually explain what's going on—`prefix` takes an `Int` (not an `Element`) and returns a `SubSequence` (not `Void`), and is also `nonmutating`. That seems like enough contextual hints to tell you what's going on.

IMO `skip`, as an alternative listed in the proposal, would convey the intention better and clearer, and it has roots in the sequence-like constructs of reactive programming libraries (e.g. RxSwift and ReactiveCocoa).

That's an interesting and very, very relevant precedent. Thanks for mentioning it!

···

--
Brent Royal-Gordon
Architechies


(Brent Royal-Gordon) #12

Addressing the same issue from several people:

I’m not a fan of the subscript solutions. They both introduce new types which seems very heavyweight for such a small use case. I’d vote for keeping the current functions.

Ditto, not a fan of a subscript solution here. It is true that `suffix(from:)` seems weird at first, but it is literally nonsensical to interpret the single argument any other way, partly because of the mandatory argument label but mostly because a "suffix" that doesn't go to the end isn't a suffix.

-1, because [separate point snipped] they match the
theme you are proposing, while subscripts do not.

Superficially, `prefix(upTo:)`, `prefix(through:)` and `suffix(from:)` appear to be prefix and suffix operations, but I think that's actually a poor way to model them.

The problem is most obvious when you look at `suffix(from:)`. Imagine you have a four-element array:

  Elements: a b c d
  Indices: [0] [1] [2] [3]

`prefix(2)` and `suffix(2)` will select the first and last two elements, respectively.

  Elements: a b c d
  Indices: [0] [1] [2] [3]
  prefix(2) ^^ ^^
  suffix(2) ^^ ^^

And for a four-element array, so will `prefix(upTo:)` and `suffix(from:)`.

  Elements: a b c d
  Indices: [0] [1] [2] [3]
  prefix(2) ^^ ^^
    upTo: 2 ^^ ^^
  suffix(2) ^^ ^^

···

On Jun 23, 2016, at 11:28 AM, David Hart <david@hartbit.com> wrote:
On Jun 23, 2016, at 10:46 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Jun 23, 2016, at 1:06 PM, Anton Zhilin via swift-evolution <swift-evolution@swift.org> wrote:

    from: 2 ^^ ^^

However, if you insert an element in the middle of the array, a funny thing happens:

  Elements: a b c e d
  Indices: [0] [1] [2] [3] [4]
  prefix(2) ^^ ^^
    upTo: 2 ^^ ^^
  suffix(2) ^^ ^^
    from: 2 ^^ ^^ ^^

Unlike the other methods, adding additional elements changed the length of `suffix(from:)`'s return value. That indicates to me that it is *not* a suffix operation at all.

Now, the simplest way to resolve this is to say that `suffix(from:)` is actually misnamed; it should be `removingPrefix(upTo:)`. That will work for arrays, but not necessarily for other collections.

For instance, I've sketched a LinkedList type here: <https://gist.github.com/brentdax/20fba60dd782045faa1cfefcde298874> The important thing to note about this type is that it uses an opaque index which always points to the same node, even if you insert or remove other nodes. Thus, unlike an array, inserting or removing an element before a particular element doesn't invalidate its index. (If you're wondering, the Collection documentation doesn't seem to imply this is a forbidden design.)

Now let's build a linked list of four elements. Since this time we have non-integer indices, I'll call them i0, i1, etc.

  Elements: a b c d
  Indices: [i0] [i1] [i2] [i3]
  prefix(2) ^^ ^^
    upTo: i2 ^^ ^^
  suffix(2) ^^ ^^
    from: i2 ^^ ^^

Now what happens if we insert a new element at `j`, a new index between `i0` and `i1`?

  Elements: a e b c d
  Indices: [i0] [j] [i1] [i2] [i3]
  prefix(2) ^^ ^^
    upTo: i2 ^^ ^^ ^^
  suffix(2) ^^ ^^
    from: i2 ^^ ^^

How about that? We see the same anomalous expansion behavior, but from the start instead of the end of the linked list. We can, of course, expand both of them:

  Elements: a e b c f d
  Indices: [i0] [j] [i1] [i2] [k] [i3]
  prefix(2) ^^ ^^
    upTo: i2 ^^ ^^ ^^
  suffix(2) ^^ ^^
    from: i2 ^^ ^^ ^^

This is not behavior you get out of *any* of the other prefix or suffix methods. What *do* you get this behavior from? Range subscripts. `linkedList[i0..<i2]` would behave in exactly the same way this does.

That's why I say in the proposal that:

prefix(upTo:), prefix(through:), and suffix(from:) at first appear to belong to the same family as the other prefix and suffix methods, but deeper examination reveals otherwise.

These operations behave differently under mutation than the other `prefix` calls, but very similarly to other range operations. Thus, I prefer to think of them as a funny range operation, not a `prefix` or `suffix` operation.

--
Brent Royal-Gordon
Architechies


#13

The incomplete range concept is quite intriguing.

Have we considered spelling the operators with an asterisk at the
incomplete end?
prefix *..<
prefix *...
postfix ...*
postfix ..<*

That way the use-sites would look like:
someCollection[*..<idx]
someCollection[*...idx]
someCollection[idx...*]
someCollection[idx..<*]

From a “first-glance” perspective, the asterisk “looks like” a wildcard

placeholder, which should help readers and writers of code to understand
the meaning.

And from a future language development standpoint, we’ll keep the
triple-dot spelling available for whatever needs may arise (tuple
splatting, variadic generics, etc.)

Thoughts?

Nevin

···

On Fri, Jul 1, 2016 at 6:50 PM, Dave Abrahams via swift-evolution < swift-evolution@swift.org> wrote:

on Thu Jun 23 2016, Brent Royal-Gordon <swift-evolution@swift.org> wrote:

> As previously threatened mentioned, I've written a draft proposal to
> fix a number of naming issues with APIs operating on the beginning and
> end of Sequences and Collections:
>
> • Inconsistent use of `prefix`/`suffix` vs. `first`/`last`
> • Confusing naming of `drop` methods
> • Ambiguous naming of `index(of:/where:)` and `drop(while:)`
> • `prefix(upTo:)`, `prefix(through:)`, and `suffix(from:)` shouldn't
> be part of this family at all
>
> To fix this, I propose:
>
> • Renaming all methods which operate on more than one element at the
> beginning/end to use "prefix" or "suffix", not "first" or "last"
> • Renaming `index(of:/where:)` to `earliestIndex(…)` and
> `first(where:)` to `earliest(where:)`

What's wrong with firstIndex(of:/where:) [and lastIndex(of:/where:)]?
That seems like a much less esoteric way to phrase it that meshes well
with the meanings of

     xs.first
     xs.indices.first

etc.

> • Renaming the `drop` methods to use `removing`

Very clever! I *like*.

> • Redesigning `prefix(upTo:)`, `prefix(through:)` and `suffix(from:)`
> as subscripts with "partial" ranges, like `people[..<idx]` or perhaps
> `people[nil..<idx]`.

Yes please; I really want this. This part is a slightly nontrivial
design problem, though. Someone should build an implementation before
the actual design is proposed. Probably the best way would be to
leave prefix and suffix alone for the moment and add/test the new
subscripts.

> Since that last point requires significant redesign, including the
> introduction of new types, I have also included an alternative design
> which uses `people[to: idx]` instead.

I really don't like using labels for this, because stride(to:) and
stride(through:) have already spawned a naming bikeshed with no clear
resolution, suggesting that no name works. Plus, the ..< operator
already implies the name.

> This proposal does not seek to add new functionality; it merely
> renames or (in the case of the "aggressive" subscript option)
> redesigns existing functionality. I do, however, discuss (without
> making many judgements about their wisdom) how these changes might
> affect the naming of functionality we might add in future versions of
> Swift.

Good.

> I would mainly like feedback on the two most open questions left in
> this proposal:
>
> • The choice of `removing` to replace `drop`

It's 100% appropriate, provided that the APIs match some corresponding
mutating remove API. Nonmutating operations are often implemented via
lazy adaptors... which a slice can be viewed to be. So I think this is
a beautiful answer.

> • The decision about whether to use `people[..<idx]`,
> `people[nil..<idx]`, or `people[to: idx]`.

I prefer how the first one reads.

> But I'd also like comments on the rest of the proposal, and on whether
> I should split the prefix(upTo:/through:)/suffix(from:) changes into a
> separate proposal from the rest.

I very much appreciate that you're addressing all of these at once.

> I suspect this will cause a firestorm of bikeshedding, so please try
> to keep your suggestions grounded. Don't just suggest a name;
> articulate why it's a better choice than what we already have or what
> this proposal suggests. Only you can prevent our first
> *three*-hundred-message bikeshedding thread.
>
> Thanks for your attention!
>
> (P.S. The proposal below includes several huge tables which may cause
> some mail clients to become very pouty and refuse to eat their
> supper. You may want to read the proposal at
> <https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430
> <https://gist.github.com/brentdax/024d26c2b68b88323989540c06261430>>
> instead.)
>
> The Sequence and Collection protocols offer a wide variety of APIs which
> are defined to operate on, or from, one end of the sequence:
>
> Operand Get Index Exclude Remove (1) Pop (1) Equate (2)

I think you want “Operation” or “Semantics” rather than “Operand” (which
means an argument to an operation)

> Fixed Size
> First 1 C.first - S.dropFirst() C.removeFirst() C.popFirst() -
> Last 1 C.last - S.dropLast() C.removeLast() C.popLast() -
> First (n: Int) S.prefix(_:slight_smile: - S.dropFirst(_:slight_smile: C.removeFirst(_:slight_smile: -
S.starts(with:)
> ...with closure S.prefix(while:) - S.drop(while:) - - S.starts
> (with:isEquivalent:)
> Last (n: Int) S.suffix(_:slight_smile: - S.dropLast(_:slight_smile: C.removeLast(_:slight_smile: - -
> ...with closure - - - - - -
> Searching From End
> First matching - C.index(of:) - - - -
> element
> ...with closure S.first(where:) C.index(where:) - - - -
> Last matching element - - - - - -
> ...with closure - - - - - -
> Based on Index
> startIndex ..< (i: Index) C.prefix(upTo:) - - - - -
> startIndex ... (i: Index) C.prefix(through:) - - - - -
> (i: Index) ..< endIndex C.suffix(from:) - - - - -
>
> I have included several blank rows for operands which fit the APIs'
patterns, even if they don't happen to have any operations currently.
>
> Type abbreviations:
>
> * S = Sequence
> * C = Collection (or a sub-protocol like BidirectionalCollection)
>
> Notes:
>
> 1 remove and pop both mutate the array to delete the indicated
element(s), but remove assumes as a precondition that the indicated
elements exist, while pop
> checks whether or not they exist.
>
> 2 String and NSString have bespoke versions of first n and last n
Equate operations, in the form of their hasPrefix and hasSuffix methods.
>
> Leaving aside the question of whether any gaps in these tables ought to
be filled, I see a number of issues with existing terminology.
>
> SVG ImageInconsistent use of prefix and suffix
>
> Some APIs which operate on a variable number of elements anchored at one
end or the other use the terms prefix or suffix:
>
> * Sequence.prefix(_:slight_smile: and Sequence.suffix(_:slight_smile:
> * Sequence.prefix(while:)
> * String.hasPrefix(_:slight_smile: and String.hasSuffix(_:slight_smile:
>
> Others, however, use first or last:
>
> * Sequence.dropFirst(_:slight_smile: and Sequence.dropLast(_:slight_smile:
> * Sequence.removeFirst(_:slight_smile: and Sequence.removeLast(_:slight_smile:
>
> Still others use neither:
>
> * Sequence.starts(with:)
> * Sequence.drop(while:)
>
> These methods are all closely related, but because of this inconsistent
terminology, they fail to form predictable method families.
>
> SVG Imagefirst has multiple meanings
>
> The word first can mean three different things in these APIs:
>
> * Just the very first element of the sequence.
>
> * A subsequence of elements anchored at the beginning of the sequence,
> as mentioned in the last point.
>
> * The first element encountered in the sequence which matches a given
> criterion when walking from the beginning of the sequence towards the
> end.
>
> It would be nice to have more clarity here.

You seem to be suggesting that a word needs to mean exactly the same
thing regardless of context. If so, I disagree. If I say “the first
element” or “the first element greater than 5” there's absolutely no
lack of clarity AFAICT. That accounts for the first and last bullets

The usage in the middle bullet is open to misinterpretation and I would
support fixing that.

     xs.removeFirst(42)

could read like, “remove the first element equal to 42.”

> SVG Imagedrop is misleading and scary
>
> In a Swift context, I believe the drop methods are actively confusing:
>
> * drop does not have the -ing or -ed suffix normally used for a
> nonmutating method.
>
> * drop has strong associations with destructive operations; it's the
> term used, for instance, for deleting whole tables in SQL. Even
> dropping would probably sound more like a mutating operation than
> alternatives.
>
> * As previously mentioned, the use of dropFirst and dropLast for
> single-drop operations and multiple-drop operations breaks up method
> families.
>
> drop, dropFirst, and dropLast are terms of art, so we allow them a
> certain amount of leeway. However, I believe the drop functions go
> well beyond what we should
> permit. They are relatively uncommon operations, associated primarily
> with functional languages rather than mainstream object-oriented or
> imperative languages, and
> their violation of the normal Swift naming guidelines is especially
> misleading.
>
> The term-of-art exception is not a suicide pact;

Tatoo that on your forehead, mister!

> it is meant to aid understanding by importing common terminology, not
> bind us to follow every decision made by any language that came before
> us. In this case, I think we should ignore precedent and forge our own
> path.
>
> SVG ImageUnstated direction of operation
>
> Several APIs could theoretically be implemented by working from either
> end of the sequence, and would return different results depending on
> the direction, but do not indicate the direction in their names:
>
> * Sequence.drop(while:)
> * Collection.index(of:)
>
> Adding a direction to these APIs would make their behavior clearer and
permit us to offer opposite-end equivalents in the future. (Unmerged
swift-evolution pull
> request 329 would add lastIndex methods.)
>
> SVG ImageThe index(...) base name has been polluted
>
> Swift 3's new collection model placed a number of low-level index
> manipulating operations on the base method name index. These now share
> that name with index(of:) and index(where:), which are much
> higher-level operations. This may be confusing for users looking for
> high-level operations; the only real relationship between the two sets
> of operations is that they both return an index.

There's another relationship. Once you call the high-level operation,
you're now in the domain of indexing, and are very likely to ask for the
index(after:) the one you found.

> It would be nice to separate these two groups of methods into
> different families.

I used to think that was important, but I no longer do given the above.

> SVG ImageOperations taking an index are really slicing
>
> prefix(upTo:), prefix(through:), and suffix(from:) at first appear to
> belong to the same family as the other prefix and suffix methods, but
> deeper examination reveals otherwise. They are the only operations
> which take indices, and they don't cleanly extend to the other
> operations which belong to these families. (For instance, it would not
> make sense to add a dropPrefix(upTo:) method; it would be equivalent
> to suffix(from:).)
>
> Also, on Int-indexed collections like Array, prefix(_:slight_smile: and
> prefix(upTo:) are identical, but there is little relationship between
> suffix(_:slight_smile: and suffix(from:), which is confusing.
>
> suffix(from:) is a particularly severe source of confusion. The other
> suffix APIs all have parameters relative to the endof the collection,
> but suffix(from:)'s index is still relative to the beginning of the
> array. This is obvious if you think deeply about the meaning of an
> index, but we don't really want to force our users to stare at a
> strange API until they have an epiphany.
>
> I believe these operations have much more in common with slicing a
> collection using a range, and that reimagining them as slicing APIs
> will be more fruitful.

Yes please.

> SVG ImageWhy does it matter?
>
> Many of these APIs are only occasionally necessary, so it's important
> that they be easy to find when needed and easy to understand when
> read. If you know that prefix (10) will get the first ten elements but
> don't know what its inverse is, you will probably not guess that it's
> dropFirst(10). The confusing, conflicting names in these APIs are a
> barrier to users adopting them where appropriate.
>
> SVG ImageProposed solution
>
> We sever the index-taking APIs from the others, forming two separate
> families, which I will call the "Sequence-end operations" and the
> "index-based operations". We then consider and redesign them along
> separate lines.
>
> SVG ImageSequence-end operations
>
> Each of these APIs should be renamed to use a directional word based
> on its row in the table:
>
> Operand Directional word
> Fixed Size
> First 1 first
> Last 1 last
> First (n: Int) prefix
> ...with closure prefix
> Last (n: Int) suffix
> ...with closure suffix
> Searching From End
> First matching element earliest
> ...with closure earliest
> Last matching element latest
> ...with closure latest
>
> To accomplish this, starts(with:) should be renamed to hasPrefix(_:),

+1

>
> and other APIs should have directional words replaced or added as
> appropriate.
>
> Additionally, the word drop in the "Exclude" APIs should be replaced
> with removing. These operations omit the same elements which the
> remove operations delete, so even though the types are not always the
> same (removing returns SubSequence, not Self), I think they are
> similar enough to deserve to be treated as nonmutating forms.
>
> These changes yield (altered names bold):
>
> Operand Get Index Exclude Remove (1) Pop (1) Equate (2)
> Fixed Size
> First 1 C.first - S.removingFirst() C.removeFirst() C.popFirst() -
> Last 1 C.last - S.removingLast() C.removeLast() C.popLast() -
> First (n: Int) S.prefix(_:slight_smile: - S.removingPrefix(_:slight_smile: C.removePrefix(_:slight_smile: -
S.hasPrefix(_:slight_smile:
> ...with closure S.prefix(while:) - S.removingPrefix - - S.hasPrefix
> (while:) (_:isEquivalent:)

Call me overly fussy, but I don't love the use of “while” here because
it seems stateful.

   xs.prefix(while: isFull)

That reads like I'm going to repeatedly take the prefix of xs while some
isFull property is true. The most descriptive usage I can think of is

   for x in xs.longestPrefix(where: isFull)

What do you think?

[BTW, you might need to stop using a table because it's already too
wide, but your examples *really* ought to be showing use cases rather
than signatures, c.f. the table in
https://github.com/apple/swift/pull/2981. Otherwise it's hard]

> Last (n: Int) S.suffix(_:slight_smile: - S.removingSuffix(_:slight_smile: C.removeSuffix(_:slight_smile: -
> - ...with closure - - - - - - Searching From End First matching -
> C.earliestIndex(of:) - - - - element ...with closure
> S.earliest(where:) C.earliestIndex - - - - (where:) Last matching
> element - - - - - - ...with closure - - - - - -
>
> SVG ImageAlternative to removing
>
> If the type differences are seen as disqualifying removing as a
> replacement for drop,

They are not!

> I suggest using skipping instead.
>
> There are, of course, many possible alternatives to skipping; this is
> almost a perfect subject for bikeshedding. I've chosen skipping
> because:
>
> 1 It is not an uncommon word, unlike (say) omitting. This means
> non-native English speakers and schoolchildren are more likely to
> recognize it.
>
> 2 It is an -ing verb, unlike (say) without. This makes it fit common
> Swift naming patterns more closely.
>
> 3 It does not imply danger, unlike (say) dropping, nor some sort of
> ongoing process, unlike (say) ignoring. This makes its behavior more
> obvious.
>
> If you want to suggest an alternative on swift-evolution, please do
> not merely mention a synonym; rather, explain why it is an improvement
> on either these axes or other ones. (I would be particularly
> interested in names other than removing which draw an analogy to
> something else in Swift.)
>
> SVG ImageIndex-based operations
>
> Because these APIs look up elements based on their indices, I believe
these operations should be exposed as subscripts, and ideally should look
like other slicing
> operations.
>
> My primary design is rather ambitious, introducing two new types and
either two operator overloads, or four unary forms of existing binary
operators. I therefore
> present a more conservative alternative as well.
>
> SVG ImagePreferred (ambitious) option
>
> let head = people[..<i]
> let tail = people[i..<]

let equivalentTail = people[i...] // reads a bit better, no?
let headThroughI = people[...i]

> let rearrangedPeople = tail + head
>
> Or this small variation:
>
> let head = people[nil ..< i]
> let tail = people[i ..< nil]
> let rearrangedPeople = tail + head
>
> The operators would construct instances of a new pair of types,
> IncompleteRange (for ..<) and IncompleteClosedRange (for ...), and
> Collection would include new subscripts taking these types. These
> would probably have default implementations which constructed an
> equivalent Range or ClosedRange using startIndex and endIndex, then
> passed the resulting range through to the existing subscripts.

W00t!

>
> I prefer this option because it offers an elegant syntax immediately
> recognizable as a form of slicing, and provides a straightforward way
> for a future version of Swift to extend other Range-handling
> Collection operations, like replaceSubrange(_:with:) and
> removeSubrange(_:), to handle subranges bound by the ends of the
> Collection.

--
Dave

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brent Royal-Gordon) #14

• Redesigning `prefix(upTo:)`, `prefix(through:)` and `suffix(from:)`
as subscripts with "partial" ranges, like `people[..<idx]` or perhaps
`people[nil..<idx]`.

Yes please; I really want this. This part is a slightly nontrivial
design problem, though. Someone should build an implementation before
the actual design is proposed. Probably the best way would be to
leave prefix and suffix alone for the moment and add/test the new
subscripts.

I'll try to figure out how to wedge something into stdlib, but until I do, here's something I tried out in a playground: https://gist.github.com/brentdax/b36ef130873b752d4c6f7ee3c157d07d

(Already sent that to you, Dave; this is for everyone else.)

• Renaming `index(of:/where:)` to `earliestIndex(…)` and
`first(where:)` to `earliest(where:)`

What's wrong with firstIndex(of:/where:) [and lastIndex(of:/where:)]?
That seems like a much less esoteric way to phrase it that meshes well
with the meanings of

    xs.first
    xs.indices.first

etc.

[and]

first has multiple meanings

The word first can mean three different things in these APIs:

* Just the very first element of the sequence.

* A subsequence of elements anchored at the beginning of the sequence,
as mentioned in the last point.

* The first element encountered in the sequence which matches a given
criterion when walking from the beginning of the sequence towards the
end.

It would be nice to have more clarity here.

You seem to be suggesting that a word needs to mean exactly the same
thing regardless of context. If so, I disagree. If I say “the first
element” or “the first element greater than 5” there's absolutely no
lack of clarity AFAICT. That accounts for the first and last bullets

I was hoping to distinguish between the O(1), always-anchored first/last calls and the O(n), unanchored earliest/latest calls. Perhaps that isn't necessary, though; `xs.first(x)` *does* read well, and it'd be difficult to imagine an implementation on most collections that didn't involve searching multiple elements.

The Sequence and Collection protocols offer a wide variety of APIs which
are defined to operate on, or from, one end of the sequence:

Operand Get Index Exclude Remove (1) Pop (1) Equate (2)

I think you want “Operation” or “Semantics” rather than “Operand” (which
means an argument to an operation)

"Operand" is meant to label the column below it, which lists things like "First 1 Element". Maybe I should just leave that cell blank, though.

The term-of-art exception is not a suicide pact;

Tatoo that on your forehead, mister!

Touché.

(I still believe map, reduce, and filter are much stronger terms of art and don't require much modification, but that's a discussion for another thread.)

The index(...) base name has been polluted

Swift 3's new collection model placed a number of low-level index
manipulating operations on the base method name index. These now share
that name with index(of:) and index(where:), which are much
higher-level operations. This may be confusing for users looking for
high-level operations; the only real relationship between the two sets
of operations is that they both return an index.

There's another relationship. Once you call the high-level operation,
you're now in the domain of indexing, and are very likely to ask for the
index(after:) the one you found.

Maybe. I suspect most users simply use the index without manipulating it, but certainly you'll sometimes use both.

(But even without that, I still think the directional vagueness and the possibility of a `lastIndex` method in the future are good enough justifications to rename it on their own.)

These changes yield (altered names bold):

Operand Get Index Exclude Remove (1) Pop (1) Equate (2)
Fixed Size
First 1 C.first - S.removingFirst() C.removeFirst() C.popFirst() -
Last 1 C.last - S.removingLast() C.removeLast() C.popLast() -
First (n: Int) S.prefix(_:slight_smile: - S.removingPrefix(_:slight_smile: C.removePrefix(_:slight_smile: - S.hasPrefix(_:slight_smile:
...with closure S.prefix(while:) - S.removingPrefix - - S.hasPrefix
  (while:) (_:isEquivalent:)

Call me overly fussy, but I don't love the use of “while” here because
it seems stateful.

  xs.prefix(while: isFull)

That reads like I'm going to repeatedly take the prefix of xs while some
isFull property is true. The most descriptive usage I can think of is

  for x in xs.longestPrefix(where: isFull)

What do you think?

I don't like changing the base name—it breaks the connection between `prefix(_:)` and `prefix(while:)`—unless we change it for all of the relevant calls, but I'm certainly open to changing the label. Maybe `prefix(whereEach: isFull)`?

On the other hand, does `xs.prefix(3)` read well? APIs that take counts seem to be challenging to name; I've had some of the same problems with UnsafeRawPointer APIs.

[BTW, you might need to stop using a table because it's already too
wide, but your examples *really* ought to be showing use cases rather
than signatures, c.f. the table in
https://github.com/apple/swift/pull/2981. Otherwise it's hard]

I'll try to find a way to fit examples in.

Preferred (ambitious) option

let head = people[..<i]
let tail = people[i..<]

let equivalentTail = people[i...] // reads a bit better, no?
let headThroughI = people[...i]

It looks nicer, but it breaks the mental model of these unary forms merely filling in `startIndex` or `endIndex` automatically. Like the `..<` operator itself, I think we're better off with ugly clarity than pretty vagueness.

···

On Jul 1, 2016, at 3:50 PM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

--
Brent Royal-Gordon
Architechies


(David Hart) #15

I don't agree with your reasoning here. It *is* an operation that returns the suffix (the end of the sequence) but starting from an index. The fact that indices index from the start of the array and that the function will return an array of a different length depending on the argument does not make the result of the operation less of a suffix. It is still returning the end of the array.

···

On 28 Jun 2016, at 13:46, Brent Royal-Gordon <brent@architechies.com> wrote:

Unlike the other methods, adding additional elements changed the length of `suffix(from:)`'s return value. That indicates to me that it is *not* a suffix operation at all.


(Patrick Smith) #16

Hi Brent,

Will an IncompleteRange always be able to be translated into the same concrete Range? i.e., the missing bound is just a stand in for startIndex or endIndex right? It seems unfortunate to have this throw away value that is only used as an intermediary. Especially when Collection already has an intermediary in the form of Index.

What if instead, you ask a collection for a prefix or suffix range from an index?

c.indexes(preceding: Index, inclusive: Bool)
c.indexes(succeeding: Index, inclusive: Bool)

These then return a Range or ClosedRange. This I believe would resolve the ‘suffix’ naming issue you have highlighted here, as instead of asking for a suffix from the *collection*, you are asking the all successors of an *index*.

Then subscripts are also added to retrieve slices. The subscripts already infer the use of indexes, so there is no need to add additional words here.

c[preceding: Index, inclusive: Bool]
c[succeeding: Index, inclusive: Bool]

This is a variation of your proposed conservative index-based operation.

Also, for the removing- base operations:

c[withoutPreceding: Index, inclusive: Bool]
c[withoutSucceeding: Index, inclusive: Bool]

c.remove(preceding: Index, inclusive: Bool)
c.remove(succeeding: Index, inclusive: Bool)

I think the words ‘preceding’ and ‘succeeding’ fit in well with the concept of ‘earliest’ and ‘latest’. Note I originally tried writing without the additional ‘inclusive’ parameter until I realised succeeding would have to be inclusive for the current suffix behaviour. It appears to make the API more complicated, but it may help to be more explicit wrt index inclusiveness that simple prefix and suffix methods ignore, and also the use of indexes instead of distances (or perhaps iteration counts?).

Hope some of this is useful! (I might be completely missing the mark)

Patrick

···

On 28 Jun 2016, at 9:46 PM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

Addressing the same issue from several people:

On Jun 23, 2016, at 11:28 AM, David Hart <david@hartbit.com> wrote:

I’m not a fan of the subscript solutions. They both introduce new types which seems very heavyweight for such a small use case. I’d vote for keeping the current functions.

On Jun 23, 2016, at 10:46 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

Ditto, not a fan of a subscript solution here. It is true that `suffix(from:)` seems weird at first, but it is literally nonsensical to interpret the single argument any other way, partly because of the mandatory argument label but mostly because a "suffix" that doesn't go to the end isn't a suffix.

On Jun 23, 2016, at 1:06 PM, Anton Zhilin via swift-evolution <swift-evolution@swift.org> wrote:

-1, because [separate point snipped] they match the
theme you are proposing, while subscripts do not.

Superficially, `prefix(upTo:)`, `prefix(through:)` and `suffix(from:)` appear to be prefix and suffix operations, but I think that's actually a poor way to model them.

The problem is most obvious when you look at `suffix(from:)`. Imagine you have a four-element array:

  Elements: a b c d
  Indices: [0] [1] [2] [3]

`prefix(2)` and `suffix(2)` will select the first and last two elements, respectively.

  Elements: a b c d
  Indices: [0] [1] [2] [3]
  prefix(2) ^^ ^^
  suffix(2) ^^ ^^

And for a four-element array, so will `prefix(upTo:)` and `suffix(from:)`.

  Elements: a b c d
  Indices: [0] [1] [2] [3]
  prefix(2) ^^ ^^
    upTo: 2 ^^ ^^
  suffix(2) ^^ ^^
    from: 2 ^^ ^^

However, if you insert an element in the middle of the array, a funny thing happens:

  Elements: a b c e d
  Indices: [0] [1] [2] [3] [4]
  prefix(2) ^^ ^^
    upTo: 2 ^^ ^^
  suffix(2) ^^ ^^
    from: 2 ^^ ^^ ^^

Unlike the other methods, adding additional elements changed the length of `suffix(from:)`'s return value. That indicates to me that it is *not* a suffix operation at all.

Now, the simplest way to resolve this is to say that `suffix(from:)` is actually misnamed; it should be `removingPrefix(upTo:)`. That will work for arrays, but not necessarily for other collections.

For instance, I've sketched a LinkedList type here: <https://gist.github.com/brentdax/20fba60dd782045faa1cfefcde298874> The important thing to note about this type is that it uses an opaque index which always points to the same node, even if you insert or remove other nodes. Thus, unlike an array, inserting or removing an element before a particular element doesn't invalidate its index. (If you're wondering, the Collection documentation doesn't seem to imply this is a forbidden design.)

Now let's build a linked list of four elements. Since this time we have non-integer indices, I'll call them i0, i1, etc.

  Elements: a b c d
  Indices: [i0] [i1] [i2] [i3]
  prefix(2) ^^ ^^
    upTo: i2 ^^ ^^
  suffix(2) ^^ ^^
    from: i2 ^^ ^^

Now what happens if we insert a new element at `j`, a new index between `i0` and `i1`?

  Elements: a e b c d
  Indices: [i0] [j] [i1] [i2] [i3]
  prefix(2) ^^ ^^
    upTo: i2 ^^ ^^ ^^
  suffix(2) ^^ ^^
    from: i2 ^^ ^^

How about that? We see the same anomalous expansion behavior, but from the start instead of the end of the linked list. We can, of course, expand both of them:

  Elements: a e b c f d
  Indices: [i0] [j] [i1] [i2] [k] [i3]
  prefix(2) ^^ ^^
    upTo: i2 ^^ ^^ ^^
  suffix(2) ^^ ^^
    from: i2 ^^ ^^ ^^

This is not behavior you get out of *any* of the other prefix or suffix methods. What *do* you get this behavior from? Range subscripts. `linkedList[i0..<i2]` would behave in exactly the same way this does.

That's why I say in the proposal that:

prefix(upTo:), prefix(through:), and suffix(from:) at first appear to belong to the same family as the other prefix and suffix methods, but deeper examination reveals otherwise.

These operations behave differently under mutation than the other `prefix` calls, but very similarly to other range operations. Thus, I prefer to think of them as a funny range operation, not a `prefix` or `suffix` operation.

--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Dave Abrahams) #17

Now it's my turn to call someone else overly fussy ;-).

People commonly talk about the first element of an array, and “earliest”
could even be confusing depending on context:

   let a = [ now(), tomorrow(), yesterday() ]

···

on Thu Jun 23 2016, Xiaodi Wu <swift-evolution@swift.org> wrote:

Yikes, not only is the email too big for some mail clients, it's too big
for the mailing list. Resending with proposal snipped.

On Thu, Jun 23, 2016 at 1:24 PM, David Hart via swift-evolution < > swift-evolution@swift.org> wrote:

Most of your proposal look great to me! Comments inline:

On 23 Jun 2016, at 09:19, Brent Royal-Gordon via swift-evolution < >> swift-evolution@swift.org> wrote:

As previously threatened mentioned, I've written a draft proposal to fix
a number of naming issues with APIs operating on the beginning and end of
Sequences and Collections:

• Inconsistent use of `prefix`/`suffix` vs. `first`/`last`
• Confusing naming of `drop` methods
• Ambiguous naming of `index(of:/where:)` and `drop(while:)`
• `prefix(upTo:)`, `prefix(through:)`, and `suffix(from:)` shouldn't be
part of this family at all

To fix this, I propose:

• Renaming all methods which operate on more than one element at the
beginning/end to use "prefix" or "suffix", not "first" or "last"

Looking at the first column in your table, I think the current API focuses

correctly on the number of elements returned (and consequently, the return
type) rather than the number of elements interrogated. "First" and "last"
on their own suggest very strongly that you get back either zero elements
or one element (thus, an optional would be the appropriate return type),
whereas "prefix" and "suffix" suggest 0 to all elements might be returned
(thus, an array or collection would be the appropriate return type).
Currently, the API adheres to that expectation as far as your "get" column
is concerned, and IMO the most consistent approach would be to whip the
remaining columns in line.

To me, `first(where:)` is unambiguous and doubly distinguished from
`prefix(while:)`--even when the argument label is dropped in trailing
closure syntax, it is clear that `first` gives you at most one, and by
contradistinction `prefix` gives you at most all. I'm not sure that
"earliest" is an improvement, since you're introducing another word and
breaking the parallels here. So on reflection, I'm satisfied that first,
prefix, last, suffix all have their place in the API. Of course, I think
you could make a case for an across-the-board renaming of "first" to
"earliest" and "last" to "latest"--in fact, there could be an argument that
given 0-based indices the word "first" is an unfortunate choice in any
case.

--
Dave


(Dave Abrahams) #18

That's an interesting possiblity, IMO.

···

on Sun Jul 03 2016, Nevin Brackett-Rozinsky <swift-evolution@swift.org> wrote:

The incomplete range concept is quite intriguing.

Have we considered spelling the operators with an asterisk at the
incomplete end?
prefix *..<
prefix *...
postfix ...*
postfix ..<*

That way the use-sites would look like:
someCollection[*..<idx]
someCollection[*...idx]
someCollection[idx...*]
someCollection[idx..<*]

From a “first-glance” perspective, the asterisk “looks like” a wildcard
placeholder, which should help readers and writers of code to understand
the meaning.

And from a future language development standpoint, we’ll keep the
triple-dot spelling available for whatever needs may arise (tuple
splatting, variadic generics, etc.)

Thoughts?

--
Dave


(Dave Abrahams) #19

• Redesigning `prefix(upTo:)`, `prefix(through:)` and `suffix(from:)`
as subscripts with "partial" ranges, like `people[..<idx]` or perhaps
`people[nil..<idx]`.

Yes please; I really want this. This part is a slightly nontrivial
design problem, though. Someone should build an implementation before
the actual design is proposed. Probably the best way would be to
leave prefix and suffix alone for the moment and add/test the new
subscripts.

I'll try to figure out how to wedge something into stdlib, but until I
do, here's something I tried out in a playground:
https://gist.github.com/brentdax/b36ef130873b752d4c6f7ee3c157d07d

(Already sent that to you, Dave; this is for everyone else.)

• Renaming `index(of:/where:)` to `earliestIndex(…)` and
`first(where:)` to `earliest(where:)`

What's wrong with firstIndex(of:/where:) [and lastIndex(of:/where:)]?
That seems like a much less esoteric way to phrase it that meshes well
with the meanings of

    xs.first
    xs.indices.first

etc.

[and]

first has multiple meanings

The word first can mean three different things in these APIs:

* Just the very first element of the sequence.

* A subsequence of elements anchored at the beginning of the sequence,
as mentioned in the last point.

* The first element encountered in the sequence which matches a given
criterion when walking from the beginning of the sequence towards the
end.

It would be nice to have more clarity here.

You seem to be suggesting that a word needs to mean exactly the same
thing regardless of context. If so, I disagree. If I say “the first
element” or “the first element greater than 5” there's absolutely no
lack of clarity AFAICT. That accounts for the first and last bullets

I was hoping to distinguish between the O(1), always-anchored
first/last calls and the O(n), unanchored earliest/latest
calls. Perhaps that isn't necessary, though; `xs.first(x)` *does* read
well, and it'd be difficult to imagine an implementation on most
collections that didn't involve searching multiple elements.

Well, it's a reasonable thing to want to distinguish, but I don't think
I want to burden every API that effectively does a linear search with
the awkwardness of “earliest” and “latest.” To me it doesn't look like
a great trade-off.

We could rename “first” and “last” so they always mean O(N) but that
seems overly fussy too.

Just IMO, of course.

The Sequence and Collection protocols offer a wide variety of APIs which
are defined to operate on, or from, one end of the sequence:

Operand Get Index Exclude Remove (1) Pop (1) Equate (2)

I think you want “Operation” or “Semantics” rather than “Operand” (which
means an argument to an operation)

"Operand" is meant to label the column below it, which lists things
like "First 1 Element".

I know that, and that's exactly why “Operand” is the wrong word here.

Maybe I should just leave that cell blank, though.

The term-of-art exception is not a suicide pact;

Tatoo that on your forehead, mister!

Touché.

(I still believe map, reduce, and filter are much stronger terms of
art and don't require much modification, but that's a discussion for
another thread.)

Indeedy.

The index(...) base name has been polluted

Swift 3's new collection model placed a number of low-level index
manipulating operations on the base method name index. These now share
that name with index(of:) and index(where:), which are much
higher-level operations. This may be confusing for users looking for
high-level operations; the only real relationship between the two sets
of operations is that they both return an index.

There's another relationship. Once you call the high-level operation,
you're now in the domain of indexing, and are very likely to ask for the
index(after:) the one you found.

Maybe. I suspect most users simply use the index without manipulating
it, but certainly you'll sometimes use both.

(But even without that, I still think the directional vagueness and
the possibility of a `lastIndex` method in the future are good enough
justifications to rename it on their own.)

+1

These changes yield (altered names bold):

Operand Get Index Exclude Remove (1) Pop (1) Equate (2)
Fixed Size
First 1 C.first - S.removingFirst() C.removeFirst() C.popFirst() -
Last 1 C.last - S.removingLast() C.removeLast() C.popLast() -
First (n: Int) S.prefix(_:slight_smile: - S.removingPrefix(_:slight_smile: C.removePrefix(_:slight_smile: - S.hasPrefix(_:slight_smile:
...with closure S.prefix(while:) - S.removingPrefix - - S.hasPrefix
  (while:) (_:isEquivalent:)

Call me overly fussy, but I don't love the use of “while” here because
it seems stateful.

  xs.prefix(while: isFull)

That reads like I'm going to repeatedly take the prefix of xs while some
isFull property is true. The most descriptive usage I can think of is

  for x in xs.longestPrefix(where: isFull)

What do you think?

I don't like changing the base name—it breaks the connection between
`prefix(_:)` and `prefix(while:)`—

Well, I'm really hoping we won't hang on to the first one forever, and
that slicing syntax will eventually work here.

unless we change it for all of the relevant calls, but I'm certainly
open to changing the label. Maybe `prefix(whereEach: isFull)`?

I think “while” is better than that one, since it implies “longest.”
However, I still think “longestPrefix...” will read much more clearly at
the call site.

On the other hand, does `xs.prefix(3)` read well?

One among many reasons I want to use slicing syntax here.

APIs that take counts seem to be challenging to name; I've had some of
the same problems with UnsafeRawPointer APIs.

A separate proposal to clean up APIs taking counts might be a good idea,
then. But let's not get too far afield in this thread.

[BTW, you might need to stop using a table because it's already too
wide, but your examples *really* ought to be showing use cases rather
than signatures, c.f. the table in
https://github.com/apple/swift/pull/2981. Otherwise it's hard]

I'll try to find a way to fit examples in.

Preferred (ambitious) option

let head = people[..<i]
let tail = people[i..<]

let equivalentTail = people[i...] // reads a bit better, no?
let headThroughI = people[...i]

It looks nicer, but it breaks the mental model of these unary forms
merely filling in `startIndex` or `endIndex` automatically.

Ah, you're right, that's a dimension of simplicity I hadn't considered
(could I have said that in a more complicated way?!)

···

on Fri Jul 08 2016, Brent Royal-Gordon <brent-AT-architechies.com> wrote:

On Jul 1, 2016, at 3:50 PM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

Like the `..<` operator itself, I think we're better off with ugly
clarity than pretty vagueness.

--
Dave


(Xiaodi Wu) #20

I was in the midst of writing a reply along the same lines, so I figured
I'd add to David's reply here. There are two characteristics I would expect
from a method named "prefix" or "suffix".

First, it should return a subsequence containing zero to count elements.
(By contrast, something named "first" should return nil or one element, but
certainly no more.)

Second, in the case of "prefix", the first element of the subsequence (if
any) should be the first element of the sequence; in the case of "suffix",
the last element of the subsequence (if any) should be the last element of
the sequence.

Now, `suffix(from:)` fulfills both of those expectations. Like David, I do
not understand how you arrive at the interpretation that a "suffix
operation" should be one that returns a fixed number of elements.

Replacing the word "suffix" with square brackets is inferior, IMO, because
at the call site the reader cannot immediately tell that the two
characteristics I named above would hold. First, depending on what is
inside the square brackets, a subscript could return a subsequence or it
could return a single element. Second, depending on what is inside the
square brackets, a subsequence could start or end anywhere. By contrast,
the words "prefix" and "suffix" clearly communicate what sort of
subsequence you'll get back without inspection of the argument.

···

On Tue, Jun 28, 2016 at 1:25 PM, David Hart <david@hartbit.com> wrote:

> On 28 Jun 2016, at 13:46, Brent Royal-Gordon <brent@architechies.com> > wrote:
>
> Unlike the other methods, adding additional elements changed the length
of `suffix(from:)`'s return value. That indicates to me that it is *not* a
suffix operation at all.

I don't agree with your reasoning here. It *is* an operation that returns
the suffix (the end of the sequence) but starting from an index. The fact
that indices index from the start of the array and that the function will
return an array of a different length depending on the argument does not
make the result of the operation less of a suffix. It is still returning
the end of the array.