Pitch: Range based Collection mutations
Introduction
Finding, replacing, removing, counting, and splitting one ordered collection based on the contents of another is extremely useful (and would fill some holes in today's String API as well). We should add facilities to BidirectionalCollection
and RangeReplaceableCollection
that allow users to do this.
Two of these additions are currently being pitched here:
Count(of:) and contains(other:) for BidirectionalCollection - Pitches - Swift Forums
I included these two methods here as well because I think it makes sense to consider them as a whole rather than individually as they should all be named more or less uniformly.
Motivation
Mutating a collection based on its contents is a common thing to do. For example, you might want to split a collection up based on a delimiter. We have this in Swift today:
func split(separator: Element, maxSplits: Int = default, omittingEmptySubsequences: Bool = default) -> [AnyBidirectionalCollection<Element>]
This is very useful, but doesn't allow you you split the collection up by another ordered collection that contains the same element. For example, it would be useful to be able to do this*:
let moons = "Metis, Adrastea, Amalthea, Thebe".split(separatedBy: ", ")
*you can do this on String
today but it requires bridging to NSString
. It also doesn't work on Array
.
It is also useful to be able to find ranges, replace, remove, count, check for containment, and split the collection based on ordered collections that contain the same type.
Proposed Solution
extension RangeReplaceableCollection where Self: BidirectionalCollection, Element: Equatable {
//remove
mutating func removeFirst<C: BidirectionalCollection>(occurrenceOf pattern: C) where C.Element == Element
mutating func removeLast<C: BidirectionalCollection>(occurrenceOf pattern: C) where C.Element == Element
mutating func removeAll<C: BidirectionalCollection>(occurencesOf pattern: C) where C.Element == Element
//replace
mutating func replaceFirst<C: BidirectionalCollection>(occurrenceOf pattern: C, with replacement: C) where C.Element == Element
mutating func replaceLast<C: BidirectionalCollection>(occurrenceOf pattern: C, with replacement: C) where C.Element == Element
mutating func replaceAll<C: BidirectionalCollection>(occurrencesOf pattern: C, with replacement: C) where C.Element == Element
}
extension BidirectionalCollection where Element: Equatable {
func contains<C: BidirectionalCollection>(occurrenceOf pattern: C) -> Bool where C.Element == Element
func count<C: BidirectionalCollection>(of pattern: C, overlapping: Bool = false) -> Int where C.Element == Element
//Ranges
func lastRange<C: BidirectionalCollection>(of pattern: C) -> Range<Index>? where C.Element == Element
func firstRange<C: BidirectionalCollection>(of pattern: C) -> Range<Index>? where C.Element == Element
func ranges<C: BidirectionalCollection>(of pattern: C, overlapping: Bool = false) -> [Range<Index>] where C.Element == Element
func split<C: BidirectionalCollection>(separatedBy pattern: C, maxSplits: Int = Int.max, omittingEmptySubsequences: Bool = true) -> [SubSequence] where C.Element == Element
}
Implementation
This change is on the large side for Standard Library additions, but I think it's worth considering these APIs together.
All these can be implemented in terms of firstRange(of:)
, thus, there are benefits to designing these all together.
Performance
Many of these methods would benefit from specializations from some of the more concrete Swift types, namely Array
and String
. Therefore, we should make the ones that make sense, customization points on the respective collection. That is, they would be requirements on the protocol with default implementations, rather than being extension methods only.
Alternatives considered
We could put all the BidirectionalCollection
methods on Collection
, none of the new methods really make sense on unordered collections like Dictionary
and Set
. BidirectionalCollection
also makes all of the last
methods much more performant as we can search from the back. There are also algorithmic benefits for specializations of these methods where having the ability to traverse the supplied collection in both directions would yield better performance.
Impact on String
Under this proposal, the following String
methods would (eventually?) be deprecated as their functionality is replaced by the above methods*:
extension String {
public func contains<T : StringProtocol>(_ other: T) -> Bool
public func components<T : StringProtocol>(separatedBy separator: T) -> [String]
public func components(separatedBy separator: CharacterSet) -> [String]
}
*Note that the only thing that will be deprecated is the extension on String that exposes these NSString
methods on String
, users who still need these could cast to NSString
and get them back.