Perhaps nobody cares any more, but I'm posting a newly refined version of the last refinement. This is based on the following principles:
-
The correct syntax to use is a subscript, so that slices can be l-values.
-
The subscript has to have a keyword, because itās impossible to guarantee that the slice expression canāt be captured in a variable (though the implementation makes it hard to do this). The subscript keyword serves to warn the reader that the execution time might be slower than the O(1) guarantee of a normal Collection
subscript.
-
There is no new syntax or operators, just a couple of contextual symbols that have ā I hope ā obvious semantics.
Design
The latest design recasts the problem in terms of anchors, rather than offsets. There are anchors for the start or end of a collection, and any Collection.Index
may be converted to an anchor. The anchors are:
.atStart
.atEnd
.at (i) // where āiā is of type Collection.Index
Anchor expressions allow for offsetting relative to anchors, via addition or subtraction of signed offsets. (Offsets that are net negative when actually applied to an index require a BidirectionalCollection.) Offset expressions look like this (where n
is an Int
expression):
.atStart + n .atStart - n
.atEnd + n .atEnd - n
.at (i) + n .at (i) - n
Anchor range expressions use anchor expressions in the obvious way, for example:
.atStart ..< .atEnd // i.e. the whole collection
.atStart + 1 ..< .atEnd - 1 // i.e. dropFirst, dropLast
.at (i) ..< .atEnd // i.e. from i to the end
.at (i) ... .at (i) + 1 // a 2-element slice starting at i
(.atStart + 1)... // ugly parens required
...(.at (i))
..<(.atEnd - 1)
Subscripting of elements with anchor expressions looks like this (where c
is a collection, bi-directional for the sake of examples):
c [anchored: .atStart + 4] // the 5th element
c [anchored: .atEnd - 1] // the last element
c [anchored: .at (i) + 1] // the element following the element at index i
and slicing with anchor ranges looks like this:
c [anchored: .atStart + 1 ..< .atEnd - 1] // i.e. c.dropFirst ().dropLast ()
c [anchored: ...(.at (i))] // ugly parens required
RangeReplaceableCollection
gets anchored variants of its index-taking methods:
c.replaceSubrange (anchored: .atStart + 2 ... .atStart + 2, with: someSequence)
c.remove (anchored: .at (i) - 1)
c.removeSubrange (anchored: (.atEnd - 4)ā¦)
RangeExpression
I gave up trying to use RangeExpression
in the implementation. The internal CollectionAnchor
type used for the bounds cannot implement Comparable
(or even Equatable
) because indices donāt carry enough state to be offset without their collection, and the offsets matter to the outcome. (Using a limited or fake Comparable
implementation causes fatal runtime errors with some valid ranges which fail the lowerBounds <= upperBounds
check.)
Instead, all of the binary and unary range operators are overloaded individually.
More Samples
Hereās a wider selection of examples, comparing the anchored syntax with the basic offsetBy syntax. (In some cases, the shortest and simplest syntax is via drop/prefix/suffix convenience methods, of course.)
Scroll the box to view.
func printS<S> (_ s: S) where S: Sequence {
for e in s {
print (e, terminator: " ")
}
print ()
}
let c = "abcdefghijklmnopqrstuvwxyz"
let c2 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
let i = c.index { $0 == "q" }!
let i2 = c2.index { $0 == 22 }!
print ("index syntax vs. anchor syntax")
print (c [c.startIndex])
print (c [anchored: .atStart])
print (c [i])
print (c [anchored: .at (i)])
print (c [c.index (c.startIndex, offsetBy: 17)])
print (c [anchored: .atStart + 17])
print (c [c.index (i, offsetBy: 2)])
print (c [anchored: .at (i) + 2])
printS (c [i...])
printS (c [anchored: (.at (i))...])
printS (c [..<i])
printS (c [anchored: ..<(.at (i))])
printS (c [...i])
printS (c [anchored: ...(.at (i))])
printS (c [c.index (i, offsetBy: 2)...])
printS (c [anchored: (.at (i) + 2)...])
printS (c [..<c.index (i, offsetBy: 2)])
printS (c [anchored: ..<(.at (i) + 2)])
printS (c [...c.index (i, offsetBy: 2)])
printS (c [anchored: ...(.at (i) + 2)])
print ("conversions")
print (c [c.index (i, offsetBy: 2)])
print (c [c.index (anchored: .at (i) + 2)])
printS (c [c.index (i, offsetBy: 2) ... c.index (c.endIndex, offsetBy: -2)])
printS (c [c.subrange (anchored: .at (i) + 2 ... .atEnd - 2)])
print ("arithmetic with anchors")
printS (c [c.startIndex ..< c.endIndex])
printS (c [anchored: .atStart ..< .atEnd])
printS (c [c.index (c.startIndex, offsetBy: 1) ..< c.index (c.startIndex, offsetBy: 5)])
printS (c [anchored: .atStart + 1 ..< .atStart + 5])
printS (c [c.index (c.startIndex, offsetBy: 1) ..< c.index (c.endIndex, offsetBy: -5)])
printS (c [anchored: .atStart - -1 ..< .atEnd - 5])
printS (c [c.index (c.endIndex, offsetBy: -20) ..< c.index (c.startIndex, offsetBy: 20)])
printS (c [anchored: .atEnd - 20 ..< .atStart + 20])
printS (c [..<(c.endIndex)])
printS (c [anchored: ..<(.atEnd)])
printS (c [...(c.index (c.startIndex, offsetBy: 4))])
printS (c [anchored: ...(.atStart + 4)])
print ("ranges mixing indices with anchors")
printS (c [i ..< c.endIndex])
printS (c [anchored: .at (i) ..< .atEnd])
printS (c [c.index (c.startIndex, offsetBy: 2) ... i])
printS (c [anchored: .atStart + 2 ... .at (i)])
printS (c [c.index (i, offsetBy: 3) ..< c.endIndex])
printS (c [anchored: .at (i) + 3 ..< .atEnd])
printS (c [c.startIndex ... c.index (i, offsetBy: -2)])
printS (c [anchored: .atStart ... .at (i) - 2])
printS (c [c.index (i, offsetBy: 1) ..< c.index (c.endIndex, offsetBy: -1)])
printS (c [anchored: .at (i) + 1 ..< .atEnd - 1])
print ("MutableCollection mutations")
var m2 = c2
var n2 = c2
m2 [m2.startIndex] = 99
n2 [anchored: .atStart] = 99
m2 [m2.index (m2.startIndex, offsetBy: 2)] = 99
n2 [anchored: .atStart + 2] = 99
m2 [m2.index (m2.endIndex, offsetBy: -4) ... m2.index (m2.endIndex, offsetBy: -1)] = [-4, -3, -2, -1]
n2 [anchored: .atEnd - 4 ... .atEnd - 1] = [-4, -3, -2, -1]
printS (m2)
printS (n2)
print ("RangeReplaceableCollection mutations")
m2.insert (42, at: m2.endIndex)
n2.insert (42, anchored: .atEnd)
m2.insert (contentsOf: [40, 41], at: m2.index (m2.endIndex, offsetBy: -1))
n2.insert (contentsOf: [40, 41], anchored: .atEnd - 1)
printS (m2)
printS (n2)
m2.replaceSubrange (m2.index (m2.startIndex, offsetBy: 2) ... m2.index (m2.startIndex, offsetBy: 2), with: [-1, -2, -3])
n2.replaceSubrange (anchored: .atStart + 2 ... .atStart + 2, with: [-1, -2, -3])
m2.remove (at: m2.index (i2, offsetBy: -1))
n2.remove (anchored: .at (i2) - 1)
m2.removeSubrange (m2.index (m2.startIndex, offsetBy: 26)...)
n2.removeSubrange (anchored: (.atStart + 26)...)
printS (m2)
printS (n2)
Implementation
// An anchor represents an index plus an offset
public struct CollectionAnchor<C: Collection> {
fileprivate enum Kind {
case start
case end
case index (C.Index)
}
fileprivate let kind: Kind
fileprivate let offset: Int
static var atStart: CollectionAnchor<C> {
return CollectionAnchor<C> (start: 0)
}
static var atEnd: CollectionAnchor<C> {
return CollectionAnchor<C> (end: 0)
}
static func at <A: Collection> (_ index: A.Index) -> CollectionAnchor<A> {
return CollectionAnchor<A> (index: index, offset: 0)
}
fileprivate init (start offset: Int) {
self.kind = .start
self.offset = offset
}
fileprivate init (end offset: Int) {
self.kind = .end
self.offset = offset
}
fileprivate init (index: C.Index, offset: Int) {
self.kind = .index (index)
self.offset = offset
}
}
// Anchors can be incremented or (for bi-directional collections) decremented by an offset
// Note that the offset parameter can be positive or negative, but a CollectionAnchor with
// a negative offset is usable only with a BidirectionalCollection
public func + <C: Collection> (lhs: CollectionAnchor<C>, rhs: Int) -> CollectionAnchor<C> {
guard rhs != 0
else { return lhs }
switch lhs.kind {
case .start:
return CollectionAnchor<C> (start: lhs.offset + rhs)
case .end:
return CollectionAnchor<C> (end: lhs.offset + rhs)
case .index (let index):
return CollectionAnchor<C> (index: index, offset: lhs.offset + rhs)
}
}
public func - <C: Collection> (lhs: CollectionAnchor<C>, rhs: Int) -> CollectionAnchor<C> {
guard rhs != 0
else { return lhs }
switch lhs.kind {
case .start:
return CollectionAnchor<C> (start: lhs.offset - rhs)
case .end:
return CollectionAnchor<C> (end: lhs.offset - rhs)
case .index (let index):
return CollectionAnchor<C> (index: index, offset: lhs.offset - rhs)
}
}
// Anchor bounds represent the starting and ending position of a range
public struct CollectionAnchorBounds<C: Collection> {
fileprivate let lowerBound: CollectionAnchor<C>
fileprivate let upperBound: CollectionAnchor<C>
fileprivate init (from lowerBound: CollectionAnchor<C>, upTo upperBound: CollectionAnchor<C>) {
self.lowerBound = lowerBound
self.upperBound = upperBound
}
fileprivate init (from lowerBound: CollectionAnchor<C>, through upperBound: CollectionAnchor<C>) {
self.lowerBound = lowerBound
self.upperBound = upperBound + 1
}
}
// Range operators construct ranges from anchors
public func ..< <C: Collection> (lhs: CollectionAnchor<C>, rhs: CollectionAnchor<C>) -> CollectionAnchorBounds<C> {
return CollectionAnchorBounds<C> (from: lhs, upTo: rhs)
}
public func ... <C: Collection> (lhs: CollectionAnchor<C>, rhs: CollectionAnchor<C>) -> CollectionAnchorBounds<C> {
return CollectionAnchorBounds<C> (from: lhs, through: rhs)
}
public postfix func ... <C: Collection> (lhs: CollectionAnchor<C>) -> CollectionAnchorBounds<C> {
return CollectionAnchorBounds<C> (from: lhs, upTo: CollectionAnchor<C> (end: 0))
}
public prefix func ..< <C: Collection> (rhs: CollectionAnchor<C>) -> CollectionAnchorBounds<C> {
return CollectionAnchorBounds<C> (from: CollectionAnchor<C> (start: 0), upTo: rhs)
}
public prefix func ... <C: Collection> (rhs: CollectionAnchor<C>) -> CollectionAnchorBounds<C> {
return CollectionAnchorBounds<C> (from: CollectionAnchor<C> (start: 0), upTo: rhs + 1)
}
// Extend protocol Collection with anchor-relative subscripts
public extension Collection {
func index (anchored anchor: CollectionAnchor<Self>) -> Self.Index {
switch anchor.kind {
case .start where anchor.offset == 0:
return startIndex
case .end where anchor.offset == 0:
return endIndex
case .index (let i) where anchor.offset == 0:
return i
case .start:
return index (startIndex, offsetBy: anchor.offset)
case .end:
return index (self.endIndex, offsetBy: anchor.offset)
case .index (let i):
return index (i, offsetBy: anchor.offset)
}
}
func subrange (anchored anchorRange: CollectionAnchorBounds<Self>) -> Range<Self.Index> {
return index (anchored: anchorRange.lowerBound) ..< index (anchored: anchorRange.upperBound)
}
subscript (anchored anchor: CollectionAnchor<Self>) -> Self.Element {
return self [index (anchored: anchor)]
}
subscript (anchored anchorRange: CollectionAnchorBounds<Self>) -> SubSequence {
return self [subrange (anchored: anchorRange)]
}
}
// Extend protocol MutableCollection with anchor-relative subscripts
public extension MutableCollection {
subscript (anchored anchor: CollectionAnchor<Self>) -> Self.Element {
get {
return self [self.index (anchored: anchor)]
}
set {
self [self.index (anchored: anchor)] = newValue
}
}
subscript (anchored anchorRange: CollectionAnchorBounds<Self>) -> SubSequence {
get {
return self [subrange (anchored: anchorRange)]
}
set {
self [subrange (anchored: anchorRange)] = newValue
}
}
}
// Extend protocol RangeReplaceableCollection with anchor-relative subscripts
public extension RangeReplaceableCollection
{
mutating func insert (_ newElement: Self.Element, anchored anchor: CollectionAnchor<Self>) {
insert (newElement, at: index (anchored: anchor))
}
mutating func insert<S> (contentsOf newElements: S, anchored anchor: CollectionAnchor<Self>)
where S : Collection, Self.Element == S.Element {
insert (contentsOf: newElements, at: index (anchored: anchor))
}
mutating func replaceSubrange<C> (anchored anchorRange: CollectionAnchorBounds<Self>, with newElements: C)
where C : Collection, Self.Element == C.Element {
replaceSubrange (subrange (anchored: anchorRange), with: newElements)
}
@discardableResult
mutating func remove(anchored anchor: CollectionAnchor<Self>) -> Self.Element {
return remove (at: index (anchored: anchor))
}
mutating func removeSubrange (anchored anchorRange: CollectionAnchorBounds<Self>) {
removeSubrange (subrange (anchored: anchorRange))
}
}