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

Brent Royal-Gordon brent at architechies.com
Fri Jul 8 21:22:07 CDT 2016

> On Jul 1, 2016, at 3:50 PM, Dave Abrahams via swift-evolution <swift-evolution at swift.org> wrote:
>> • 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.


>> 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!


(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(_:) - 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?

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.

Brent Royal-Gordon

More information about the swift-evolution mailing list