Renaming subranges(where:/of:)

Greetings! Here's a very small pitch to rename the subranges(where:) and subranges(of:) methods, approved as part of SE-0270, to indices(where:) and indices(of:).

Motivation

Though approved and implemented at the beginning of last year, the new APIs in SE-0270 have yet to land in a shipping version of Swift, due to concerns about naming. In particular, we've had questions about whether subranges is the appropriate name for the methods that find all instances of a particular element, or all elements that match a given predicate.

In the interim, we've launched the Algorithms package and continued exploration of searching within collections. This research has pointed out out a distinction between the methods in SE-0270, which return a value that represents all matching indices, and searching algorithms, which, depending on the design, can return the matched subsequences of a collection or the ranges of those matches. That is to say, the unit of the returned values differ: the SE-0270 methods return indices, while subsequence searching methods return ranges.

This distinction points to a need for a better name for the existing subranges(where:) and subranges(of:) methods, to make room for future collection searching operations.

Proposed solution

The two methods in questions will be renamed to indices(where:) and indices(of:). In addition to solving the problem described above, this brings these methods inline with their documentation, which describes the methods as returning the indices of the matching elements.

Detailed design

The new versions of these two methods are as follows:

extension Collection {
  /// Returns the indices of all the elements that match the given predicate.
  ///
  /// For example, you can use this method to find all the places that a
  /// vowel occurs in a string.
  ///
  ///     let str = "Fresh cheese in a breeze"
  ///     let vowels: Set<Character> = ["a", "e", "i", "o", "u"]
  ///     let allTheVowels = str.indices(where: { vowels.contains($0) })
  ///     // str[allTheVowels].count == 9
  ///
  /// - Parameter predicate: A closure that takes an element as its argument
  ///   and returns a Boolean value that indicates whether the passed element
  ///   represents a match.
  /// - Returns: A set of the indices of the elements for which `predicate`
  ///   returns `true`.
  ///
  /// - Complexity: O(*n*), where *n* is the length of the collection.
  public func indices(where predicate: (Element) throws -> Bool) rethrows
    -> RangeSet<Index>
}

extension Collection where Element: Equatable {
  /// Returns the indices of all the elements that are equal to the given
  /// element.
  ///
  /// For example, you can use this method to find all the places that a
  /// particular letter occurs in a string.
  ///
  ///     let str = "Fresh cheese in a breeze"
  ///     let allTheEs = str.indices(of: "e")
  ///     // str[allTheEs].count == 7
  ///
  /// - Parameter element: An element to look for in the collection.
  /// - Returns: A set of the indices of the elements that are equal to
  ///   `element`.
  ///
  /// - Complexity: O(*n*), where *n* is the length of the collection.
  public func indices(of element: Element) -> RangeSet<Index>
}

Source compatibility

Because these methods have not shipped in a Swift release, this will have no source compatibility impact for code that relies only on the standard library. The SE-0270 preview package will be updated to include the new names along with deprecated methods with the old names.

Effect on ABI stability

The existing methods are not yet part of any Swift ABI. Once landed in a release, indices(where:) and indices(of:) will become part of the ABI.

Effect on API resilience

The methods with new names will continue to have the same treatment as the rest of SE-0270's additions.

11 Likes

To me, the name indices suggests that I will be able to iterate over them, as in

let array = [1, 2, 3]
for i in array.indices {
    // ...
}

— I couldn't find in the preview package or in the proposal any mention of RangeSet itself conforming to Sequence or any similar behaviour being possible. Am I looking in a wrong place (I haven't read through the past proposal reviews)? Otherwise, I would find this naming very confusing and inconsistent with the current indices property.

1 Like

It is true that it shares a name with the indices property. You can use the result in a subscript to iterate over the elements:

let evens = numbers.indices(where: { $0.isMultiple(of: 2) })
for n in numbers[evens] {
    // do something with n
}
1 Like

+1

I was in favor of the indices spelling during the original review, and I continue to believe it is the clearest and most natural at the point of use.

Additionally, the original proposal introduces removeSubranges(_:) and other similar methods. To me,

array.removeSubranges(array.indices(where: ...))

reads less consistently than with subranges(where:/of:), although I admit that there's not much room for any other interpretation.

Still, should then all "subranges"-based methods get renamed to "indices"-based? If so, how should they be called?

Well, indeed. What I wanted to address is the actual discrepancy between what's already very common and what's being proposed. There are a lot of things that you can do with the "normal" indices, like the mentioned iteration, getting some particular values like indices.first or all those functional methods like filter, map etc., which you suddenly can't do with indices(where:/of:). The word "indices" just bears a lot of additional context and assumptions in Swift, and while in a purely technical sense RangeSet too represents some aggregation of indices, it is a very restrictive representation.

Just by looking at the name, it is very natural to assume that if something like

collection.indices.map { ... }.shuffled()

(regardless of how much sense it actually makes) can be written, then

collection.indices(where: { ... }).map { ... }.shuffled()

should be able to be written too, but suddenly one needs to either look into the documentation for virtually any "indices" method/property or consult the typechecker to see if a particular "indices" method returns a collection type or not.

There are a lot of assumptions that will be broken if the proposed naming gets accepted, and I believe it's worth discussing if they should be broken at all.

3 Likes

On a practical level: I know that we've been unable to add things like count(where:) because of the impact on type checker performance given the existence of count. Would adding indices(where:) not create the same problem given the existence of indices?

But I would also second @wtedst's concern. I know we had an extensive discussion at the time regarding whether RangeSet properly returns a collection of ranges or of indices, and the proposed renaming here makes that problem more acute, because the property strongly suggests the latter but the type does not deliver on that promise.

2 Likes
Terms of Service

Privacy Policy

Cookie Policy