Skip to content

proposal: x/exp/slices: add First and Last #53510

Closed
@dsnet

Description

@dsnet

The pattern s[len(s)-1] appears fairly often.
However, s[len(s)-1] is neither readable nor easy to type,
especially if s is a relatively complex expression.

Thus, I propose the addition of slices.Last that
retrieves the last elements, respectively.
I also propose adding slices.First for consistency.

Supporting evidence

Frequency of constant slice indexes relative to start or end:

Index Occurrences Percentage
0 4953854 41.684%
1 2192516 18.449%
2 1135707  9.556%
3 1370480 11.532%
4 521199  4.386%
other 1391696 11.710%
-5 196  0.002%
-4 443  0.004%
-3 1682  0.014%
-2 10336  0.087%
-1 306168  2.576%

Even though obtaining the last element is an order-of-magnitude
less frequent than getting the first element,
there are still ~300k occurences.

Of the number of cases where s[len(s)-1] appears,
it appears alongside s[0] 78% of the time.
For example:

minTime, _ := extractTimeRange(lastShard.spans[0])
_, maxTime := extractTimeRange(lastShard.spans[len(lastShard.spans)-1])

For this reason, I propose the addition of slices.First,
so that these case will read naturally:

minTime, _ := extractTimeRange(slices.First(lastShard.spans))
_, maxTime := extractTimeRange(slices.Last(lastShard.spans))

Considerations

This API does not help the following statement:

s[len(s)-1] = ...

since the returned value of slices.Last cannot be assigned to.
This occurs 32344 times (10.6%).

It also does not help the following expression:

&s[len(s)-1]

since you cannot take the address of the return value of slice.Last.
This occurs 8102 times (2.6%).

To support these 13% of cases, we could have slices.Last
return a pointer to the element, so that you could do:

*slices.Last(s) = ...

However, that is clunky to use.

Alternatives

Last is unhelpful if you want the penultimate element (i.e., s[len(s)-2]),
but there are an order-of-magnitude fewer use-cases for that compared
to just getting the last element.

We could consider a Python-like indexing operation with negative indexing:

// At retrieves the element at i.
// If i is negative, then it is indexed relative to the end.
func At[S ~[]E, E any](s S, i int) E

This provides a general-purpose way to retrieve elements relative to the end.

Negative indexing has been proposed many times in the past for slicing and was rejected
because it is easy for programming bug to go unnoticed.
Using an explicit API like slices.At could be clear enough signal that we truly want
negative indexing. Perhaps that is an appropriate trade-off.

I personally would like to have At as well,
but the evidence does not seem to indicate that it is as prevalent
compared to the need for Last.

\cc @ianlancetaylor @eliben

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions