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

Dave Abrahams dabrahams at apple.com
Mon Jul 11 13:04:22 CDT 2016


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 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.
>
> [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(_:) - 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:)`—

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

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

-- 
Dave


More information about the swift-evolution mailing list