+šÆ
Given this topic isnāt here the first time, maybe it helps to identify hotspots of contention and confusion:
Collectionās slicing overloads for Sequence methods (prefix, suffix, dropFirst, dropLast) ā general unification of these to adopt subscript as shorthand syntax
- Concerns over performance characteristics of subscript arithmetic for non-Integer backed indices in light of performance guarantees vis-Ć -vis
Collection protocol family
- Mutating subscripted slice @dabrahams brought up
I have re-read this thread again, and it is not clear to me what would be reasons against adopting something like @Letan's .. solution as relative arithmetic against start index and end indexes. I would maybe change the name to SliceRange and propose we borrow Python's terminology for these operations on Sequence and Collection, calling it slicing.
I believe this is closes to what @ben-cohen and @dabrahams were describing as future directions in Strings in Swift 4 :
Slicing a Sequence
SliceRange describes Sequence operations that return subsequence relative to the specified bounds. It is formed using the .. operator from integer bounds. Positive bounds are relative to the start of the sequence. Negative bounds are relative to the end of the sequence.
Half-Bounded SliceRange
Sequence Slice |
Condition |
Equivalent Sequence Operations |
s[i..] |
0 <= i |
s.dropFirst(i) |
|
i < 0 |
s.suffix(abs(i)) |
s[..i] |
0 <= i |
s.prefix(i) |
|
i < 0 |
s.dropLast(abs(i)) |
| Edit: fixed, thanks to @QuinceyMorris |
|
|
Bounded SliceRange : s[i..j]
| Conditions |
0 <= j |
j < 0 |
0 <= i |
s.prefix(j).dropFirst(i) |
s.dropFirst(i).dropLast(abs(j)) |
i < 0 |
unsupported |
s.suffix(abs(i)).dropLast(abs(j)) |
The SliceRange combination with lowerBound from end of the sequence and upperBound from start of sequence is not supported for Sequence, because it can not be expressed in terms of successive sequence operations without knowing the sequence length. This limitation could be potentially lifted, if I wasnāt aiming to express the slices in terms of sequence ops. The suffix implementation is accumulating data in buffer, keeping an element count from the beginning to trim the resulting sequence is doable.
Hereās fully working implementation sketch Iāve put together in a Playground:
infix operator .. : RangeFormationPrecedence
prefix operator ..
postfix operator ..
prefix operator ..-
infix operator ..- : RangeFormationPrecedence
public struct SliceRange {
var lowerBound: Int
var upperBound: Int?
}
func ..(lhs: Int, rhs: Int) -> SliceRange {
return SliceRange(lowerBound: lhs, upperBound: rhs)
}
prefix func ..(upperBound: Int) -> SliceRange {
return SliceRange(lowerBound: 0, upperBound: upperBound)
}
postfix func ..(lowerBound: Int) -> SliceRange {
return SliceRange(lowerBound: lowerBound, upperBound: nil)
}
// Negating `SliceRange` to allow slices with lower bound
// relative to the end of `Sequence`
public prefix func -(slice: SliceRange) -> SliceRange {
return SliceRange(lowerBound: -slice.lowerBound,
upperBound: slice.upperBound)
}
// Resolves unary operator juxtaposition of `..` and `-` for
// `SliceRange` with upper bound from the end of `Sequence`
public prefix func ..-(upperBound: Int) -> SliceRange {
return ..(-upperBound)
}
public func ..-(lowerBound: Int, upperBound: Int) -> SliceRange {
return lowerBound..(-upperBound)
}
extension Sequence {
subscript(slice: SliceRange) -> SubSequence {
switch (slice.lowerBound, slice.upperBound) {
case (0, .some(let upTo)) where 0 <= upTo:
return self.prefix(upTo)
case (0, .some(let n)) where n < 0:
return self.dropLast(-n)
case (let n, nil) where 0 <= n:
return self.dropFirst(n)
case (let length, nil) where length < 0:
return self.suffix(-length)
case (let n, .some(let upTo)) where 0 <= n && 0 <= upTo:
// return self.prefix(upTo).dropFirst(n)
// Value of type 'Self.SubSequence' has no member 'dropFirst'
return (self.prefix(upTo) as! AnySequence<Element>)
.dropFirst(n) as! Self.SubSequence
case (let n, .some(let upTo)) where 0 <= n && upTo < 0:
// return self.dropFirst(n).dropLast(-upTo)
return (self.dropFirst(n) as! AnySequence<Element>)
.dropLast(-upTo) as! Self.SubSequence
case (let length, .some(let upTo)) where length < 0 && 0 <= upTo:
fatalError("Unsuported SliceRange combination for Sequence: lowerBound from end of the sequence and upperBound from start of sequence.")
case (let n, .some(let upTo)) where n < 0 && upTo < 0:
// XXX Why is the forced cast suggested by compiler necessary???
return self.suffix(-n).dropLast(-upTo) as! Self.SubSequence
default:
fatalError("Unexpected combination: " + String(describing: slice))
}
}
}
// Tests
let seq = sequence(first: 0) { $0 < 6 - 1 ? $0 &+ 1 : nil }
print(Array( seq )) // [0, 1, 2, 3, 4, 5]
print(Array( seq[2..] )) // [2, 3, 4, 5]
print(Array( seq[..2] )) // [0, 1]
print(Array( seq[(-2)..] )) // [4, 5]
print(Array( seq[-2..] )) // [4, 5]
print(Array( seq[..(-2)] )) // [0, 1, 2, 3]
print(Array( seq[..-2] )) // [0, 1, 2, 3]
print(Array( seq[2..4] )) // [2, 3]
print(Array( seq[2..(-1)] )) // [2, 3, 4]
print(Array( seq[2..-1] )) // [2, 3, 4]
//print(Array( seq[-5..3] )) // unsupported combination
print(Array( seq[-3..(-1)] )) // [3, 4]
print(Array( seq[-3..-1] )) // [3, 4]
Slicing a Collection
I donāt have an up-to-date master build on hand, but I donāt seen any fundamental obstacles to using @Letanās code:
extension SliceRange: RangeExpression {
public func relative<C: Collection>(to collection: C) -> Range<Bound>
where C.Index == Bound {
let startBase = lowerBound < 0
? collection.endIndex
: collection.startIndex
let endBase = upperBound == nil || upperBound! < 0
? collection.endIndex
: collection.startIndex
let start = collection.index(startBase, offsetBy: lowerBound)
let end = collection.index(endBase, offsetBy: upperBound ?? 0)
return start..<end
}
}
To address the performance concerns, we should clearly document that SliceRange conversion to Range (via RangeExpression protocol conformance) performs computation whose performance guarantees are given by the underlying collection.
Iām not sure if the above slicing implementation for Collections automatically works for mutable range modifications as @dabrahams mentions above⦠Does it?