Hello everyone, I’d like to throw out a simplified offset-based indexing and subscript that I think is pretty minimal and straight-forward. There are some design questions involved that can only really be answered through actual usage. Please copy this code into your projects and have fun; let us know how you use it!
Also, I’m looking for a co-author (or primary author) on this pitch who believes in this minimalistic approach. There’s a (albeit narrow) window in which we could squeeze this in for Swift 5.1, but that would require a diligent co-author who can help sort out these issues and feedback from the review. Let me know if you’re interested!
Some points worth exploring
Should the result of subscript(offset:)
be optional?
An optional result allows us to avoid having to explicitly guard against the length all over the place, and is a frequently requested addition to even Array
. For String, optionality allows us to avoid the O(n)
scan to answer count
.
But, if most usage is in a context where we already know the length, then optionality will result in spurious !
s littered throughout the code.
Related, what about index(atOffset:)
? Optional seems a little more appropriate and aligns with index(_:offsetBy:limitedBy:)
.
Should index(atOffset:)
return nil
instead of endIndex
if passed count
?
endIndex
is not part of indices
, but is used as an upper bound for access. Should we consider count
to be an invalid offset, index(atOffset:)
should return nil rather than endIndex
, which would be consistent with subscript.
What other kinds of benefits can you think of (outside of String)?
Offset-based subscripting seems like it would benefit slices of random-access collections. Integer indices for slices of RACs are relative to the base’s start rather than the slice’s. This can be especially problematic for Data, which is its own slice type. Would this avoid bugs or be useful in your code?
The code
extension Collection {
internal func _range(fromOffset range: Range<Int>) -> Range<Index> {
let lower = index(atOffset: range.lowerBound) ?? endIndex
let upper = index(lower, offsetBy: range.count, limitedBy: endIndex) ?? endIndex
return lower..<upper
}
internal func _range(fromOffset range: ClosedRange<Int>) -> Range<Index> {
return _range(fromOffset: range.lowerBound..<(range.upperBound &+ 1))
}
internal func _range(fromOffset range: PartialRangeUpTo<Int>) -> Range<Index> {
return _range(fromOffset: 0..<range.upperBound)
}
internal func _range(fromOffset range: PartialRangeThrough<Int>) -> Range<Index> {
return _range(fromOffset: 0...range.upperBound)
}
internal func _range(fromOffset range: PartialRangeFrom<Int>) -> Range<Index> {
return (index(atOffset: range.lowerBound) ?? endIndex)..<endIndex
}
public func index(atOffset offset: Int) -> Index? {
return index(startIndex, offsetBy: offset, limitedBy: endIndex)
}
public func offset(of index: Index) -> Int {
return distance(from: startIndex, to: index)
}
public subscript(offset offset: Int) -> Element? {
guard let idx = index(atOffset: offset), idx != endIndex else { return nil }
return self[idx]
}
public subscript(offset offset: Range<Int>) -> SubSequence {
return self[_range(fromOffset: offset)]
}
public subscript(offset offset: ClosedRange<Int>) -> SubSequence {
return self[_range(fromOffset: offset)]
}
public subscript(offset offset: PartialRangeUpTo<Int>) -> SubSequence {
return self[_range(fromOffset: offset)]
}
public subscript(offset offset: PartialRangeThrough<Int>) -> SubSequence {
return self[_range(fromOffset: offset)]
}
public subscript(offset offset: PartialRangeFrom<Int>) -> SubSequence {
return self[_range(fromOffset: offset)]
}
}
extension MutableCollection {
public subscript(offset offset: Int) -> Element? {
get {
guard let idx = index(atOffset: offset), idx != endIndex else { return nil }
return self[idx]
}
set {
guard let idx = index(atOffset: offset), idx != endIndex && newValue != nil
else { return }
self[idx] = newValue!
}
}
public subscript(offset offset: Range<Int>) -> SubSequence {
get {
return self[_range(fromOffset: offset)]
}
set {
self[_range(fromOffset: offset)] = newValue
}
}
public subscript(offset offset: ClosedRange<Int>) -> SubSequence {
get {
return self[_range(fromOffset: offset)]
}
set {
self[_range(fromOffset: offset)] = newValue
}
}
public subscript(offset offset: PartialRangeUpTo<Int>) -> SubSequence {
get {
return self[_range(fromOffset: offset)]
}
set {
self[_range(fromOffset: offset)] = newValue
}
}
public subscript(offset offset: PartialRangeThrough<Int>) -> SubSequence {
get {
return self[_range(fromOffset: offset)]
}
set {
self[_range(fromOffset: offset)] = newValue
}
}
public subscript(offset offset: PartialRangeFrom<Int>) -> SubSequence {
get {
return self[_range(fromOffset: offset)]
}
set {
self[_range(fromOffset: offset)] = newValue
}
}
}
Example use
let str = "abcde\u{301}fg"
print("str elements")
print(str[offset: 0])
print(str[offset: 3])
print(str[offset: 4])
print(str[offset: 5])
print(str[offset: 6])
print(str[offset: 7])
print(str[offset: 8])
print(str[offset: 9])
print("str slices")
print("1")
print(str[offset: 0..<str.count])
print(str[offset: 0...str.count])
print(str[offset: 0...1_000_000])
print("2")
print(str[offset: 3..<str.count])
print(str[offset: 3...str.count])
print(str[offset: 3...1_000_000])
print("3")
print(str[offset: 3..<4])
print(str[offset: 3...4])
print(str[offset: 3...(4+1)])
print("4")
print(str[offset: ..<5])
print(str[offset: ...5])
print(str[offset: ...str.count])
print(str[offset: ...1_000_000])
print("5")
print(str[offset: 4...])
print(str[offset: str.count...])
print(str[offset: 1_000_000...])
var arr = [1,2,3,4,5,6]
arr[offset: arr.count] = 8
print(arr)
arr[offset: 3] = 8
print(arr)
arr[offset: 2...] = Array(arr[offset: 2...].reversed())[...]
print(arr)
print("slice")
arr = [1,2,3,4,5,6]
print(arr[2...][3])
print(arr[2...][offset: 3])
Finally
Big shout-out to @Letan, @dabrahams, @QuinceyMorris, @xwu, @Karl, @nnnnnnnn, @SDGGiesbrecht, @itaiferber, and apologies to whoever I missed. Every one of you have contributed to this area, and I’d like to acknowledge your work in the eventual pitch. This would not preclude a future direction involving a slicing DSL, offsets-from-end, cleaning up RangeExpression, etc.