[swift-evolution] [Draft] Rationalizing Sequence end-operation names

Dave Abrahams dabrahams at apple.com
Fri Jul 1 17:50:53 CDT 2016


on Thu Jun 23 2016, Brent Royal-Gordon <swift-evolution at 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(_:) - S.dropFirst(_:) C.removeFirst(_:) - S.starts(with:)  
> ...with closure S.prefix(while:) - S.drop(while:) - - S.starts  
>       (with:isEquivalent:)  
> Last (n: Int) S.suffix(_:) - S.dropLast(_:) C.removeLast(_:) - -  
> ...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(_:) and Sequence.suffix(_:)
> * Sequence.prefix(while:)
> * String.hasPrefix(_:) and String.hasSuffix(_:)
> 
> Others, however, use first or last:
> 
> * Sequence.dropFirst(_:) and Sequence.dropLast(_:)
> * Sequence.removeFirst(_:) and Sequence.removeLast(_:)
> 
> 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(_:) and
> prefix(upTo:) are identical, but there is little relationship between
> suffix(_:) 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(_:) - S.removingPrefix(_:) C.removePrefix(_:) - S.hasPrefix(_:)  
> ...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(_:) - S.removingSuffix(_:) C.removeSuffix(_:) -
> - ...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



More information about the swift-evolution mailing list