Pitch: Range return values for String Insert/Replace methods

MOTIVATION:

Swift strings support a few methods for inserting and replacing things in the middle of them:

mutating func insert(_ newElement: Character, at i: String.Index)

mutating func insert<S : Collection where S.Iterator.Element == Character>(contentsOf newElements: S, at i: String.Index)

mutating func replaceSubrange(_ bounds: ClosedRange<String.Index>, with newElements: String)

mutating func replaceSubrange(_ bounds: Range<String.Index>, with newElements: String)

mutating func replaceSubrange<C where C : Collection, C.Iterator.Element == Character>(_: ClosedRange<String.Index>, with: C)

mutating func replaceSubrange<C where C : Collection, C.Iterator.Element == Character>(_ bounds: Range<String.Index>, with newElements: C)

These work well, but sometimes is advantageous to know the range of the portion of the string taken up by the inserted elements—for example, if one is creating a UI element, one may want to highlight the inserted text. In the old days when strings were defined in terms of UTF-16 code units, this was easy enough—just make a range starting at the insertion point and with the length of the string you just inserted. In Swift it is awkward and verbose:

let insertedRange = replacementRange.lowerBound..<string.index(replacementRange.lowerBound, offsetBy: replacementString.distance(from: replacementString.startIndex, to: replacementString.endIndex))

PROPOSED SOLUTION:

Introduce a return value on the insert and replace methods that contains the range of the inserted characters or string:

@discardableResult mutating func insert(_ newElement: Character, at i: String.Index) -> Range<String.Index>

@discardableResult mutating func insert<S : Collection where S.Iterator.Element == Character>(contentsOf newElements: S, at i: String.Index) -> Range<String.Index>

@discardableResult mutating func replaceSubrange(_ bounds: ClosedRange<String.Index>, with newElements: String) -> Range<String.Index>

@discardableResult mutating func replaceSubrange(_ bounds: Range<String.Index>, with newElements: String) -> Range<String.Index>

@discardableResult mutating func replaceSubrange<C where C : Collection, C.Iterator.Element == Character>(_: ClosedRange<String.Index>, with: C) -> Range<String.Index>

@discardableResult mutating func replaceSubrange<C where C : Collection, C.Iterator.Element == Character>(_ bounds: Range<String.Index>, with newElements: C) -> Range<String.Index>

The return value would contain the range of the portion of the string now occupied by what was inserted.

If this change is considered acceptable, it could also be applied to the equivalent methods on RangeReplaceableCollection.

IMPACT ON EXISTING CODE:

The @discardableResult attribute would ensure that existing code would continue to work as is. The change in the signatures, however, would affect ABI compatibility.

If the change is made to the protocols, however, this would affect source compatibility, as concrete types conforming to the protocols would need to be updated.

ALTERNATIVES CONSIDERED:

If there are performance concerns with generating the ranges, String could simply have two versions of each method, one returning a range and one returning Void.

Charles

Hi Charles,

As part of the String redesign in Swift 4, these methods on String are intended to be part of String’s conformance to RangeReplaceableCollection. As such, this proposal should be phrased in terms of a change to that protocol, rather than to String.

Couple of additional things to note regarding this:
- this would also need to consider the source compatibility consequences for any existing adopters of RRC.
- RRC is designed such that the minimum you need to implement is a single replaceSubrange call (and an init) and you get the other methods "for free". See "Conforming to the RangeReplaceableCollection Protocol” in the docs for details: RangeReplaceableCollection | Apple Developer Documentation, and the implementation for gorier details: https://github.com/apple/swift/blob/master/stdlib/public/core/RangeReplaceableCollection.swift.gyb

Regards,
Ben

···

On Mar 14, 2017, at 9:21 AM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

MOTIVATION:

Swift strings support a few methods for inserting and replacing things in the middle of them:

mutating func insert(_ newElement: Character, at i: String.Index)

mutating func insert<S : Collection where S.Iterator.Element == Character>(contentsOf newElements: S, at i: String.Index)

mutating func replaceSubrange(_ bounds: ClosedRange<String.Index>, with newElements: String)

mutating func replaceSubrange(_ bounds: Range<String.Index>, with newElements: String)

mutating func replaceSubrange<C where C : Collection, C.Iterator.Element == Character>(_: ClosedRange<String.Index>, with: C)

mutating func replaceSubrange<C where C : Collection, C.Iterator.Element == Character>(_ bounds: Range<String.Index>, with newElements: C)

These work well, but sometimes is advantageous to know the range of the portion of the string taken up by the inserted elements—for example, if one is creating a UI element, one may want to highlight the inserted text. In the old days when strings were defined in terms of UTF-16 code units, this was easy enough—just make a range starting at the insertion point and with the length of the string you just inserted. In Swift it is awkward and verbose:

let insertedRange = replacementRange.lowerBound..<string.index(replacementRange.lowerBound, offsetBy: replacementString.distance(from: replacementString.startIndex, to: replacementString.endIndex))

PROPOSED SOLUTION:

Introduce a return value on the insert and replace methods that contains the range of the inserted characters or string:

@discardableResult mutating func insert(_ newElement: Character, at i: String.Index) -> Range<String.Index>

@discardableResult mutating func insert<S : Collection where S.Iterator.Element == Character>(contentsOf newElements: S, at i: String.Index) -> Range<String.Index>

@discardableResult mutating func replaceSubrange(_ bounds: ClosedRange<String.Index>, with newElements: String) -> Range<String.Index>

@discardableResult mutating func replaceSubrange(_ bounds: Range<String.Index>, with newElements: String) -> Range<String.Index>

@discardableResult mutating func replaceSubrange<C where C : Collection, C.Iterator.Element == Character>(_: ClosedRange<String.Index>, with: C) -> Range<String.Index>

@discardableResult mutating func replaceSubrange<C where C : Collection, C.Iterator.Element == Character>(_ bounds: Range<String.Index>, with newElements: C) -> Range<String.Index>

The return value would contain the range of the portion of the string now occupied by what was inserted.

If this change is considered acceptable, it could also be applied to the equivalent methods on RangeReplaceableCollection.

IMPACT ON EXISTING CODE:

The @discardableResult attribute would ensure that existing code would continue to work as is. The change in the signatures, however, would affect ABI compatibility.

If the change is made to the protocols, however, this would affect source compatibility, as concrete types conforming to the protocols would need to be updated.

ALTERNATIVES CONSIDERED:

If there are performance concerns with generating the ranges, String could simply have two versions of each method, one returning a range and one returning Void.

Charles

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution