I like your proposal, Letanyan, but shouldn't the mutable subscript be defined on RangeReplaceableCollection instead of MutableCollection? A MutableCollection can only be mutated one element at a time, and you cannot replace arbitrary subranges – that is exactly what RangeReplaceableCollection is for.
Since String conforms to RangeReplaceableCollection, but not MutableCollection, that would allow you to do things like:
var string = "Hello world!"
string[offset: 6..<11] = "universe"
This looks pretty nice. My only concern is the impact on type checker performance of another integer expressible type. But the compiler performance tests should tell us if that's going to be a problem.
The more I think about it, the more convinced I become that dropFirst() and friends are the right thing to use. They make it natural and convenient to extract the portions of a collection you want, while maintaining O(n) complexity for traversal algorithms.
They certainly are very well-thought-out APIs, and they have their place. But even outside of slicing - just accessing a single element by an index + offset (which I find to not be so uncommon) is extremely verbose.
Now, it is indeed very explicit about the index traversal operation that will be happening, but it's also a pain in the backside. I'm sure the status quo isn't the best solution we can think of.
I mean, really that's what it comes down to. We have all these operations today, they're just not very pleasant to use or easy to read.
public typecopy IndexOffset: Int, Comparable, ExpressibleByIntegerLiteral {
public publish Equatable
public publish ExpressibleByIntegerLiteral
public static func <(lhs: IndexOffset, rhs: IndexOffset) -> Bool {
let (a, b) = (lhs.rawValue, rhs.rawValue)
return (a < b) != ((a < 0) != (b < 0)) // xor
}
}
(Just realized we should have public/internal/etc. for publishing directives.)
(Just realized again: should automatic conformance apply to type-copies that specify Equatable/Hashable/Codable, or should an explicit “publish” be required? I don’t which one would be more surprising.)
Having used many languages that have this feature I am against negative indices counting from the end since it has proven to make 'out-by-one' errors hard to debug.
I think to be able to include the ability to offset from the endIndex we should avoid ranges altogether. Instead, we should use a (Int, Int) to represent the offset amounts.
This would lead to usage looking like such:
var s = "Hello, World!"
s[offset: (7, -1)] = "Swift" // Hello, Swift!
This has the drawbacks of not being able to represent partial ranges, as well as, not having inclusivity options.
Can you explain why negative indexes are any more error-prone (in terms of off-by-one errors) than positive indexes? I’ve found them very useful, and haven’t run into problems like this.
I do think it's less obvious whether you want inclusive or exclusive ranges than when you're thinking forwards. And there's also the question of whether -i>... should be a thing.
I think so. I'm reminded of Dave Abraham's suggestion to use $ and ^ symbols to represent offsets from the start or end, as in $1...^3, which seemed (and seems) a little too cryptic (and Perl-ish) to me. I think being able to explicitly write out the words "start" and "end" is actually more feature than bug.
Off-by-one are normally detected by an out of bounds when you step through all the valid indices. However if -1 is now valid you don't detect out by -1 even if you do test the first index because it wraps. Essentially you have eliminated 50% of your error checking.
Right. The key point here, that I hadn't yet considered, is that negative indexing looks obvious and natural when your range is made from integer literals, but as soon as you start computing the bounds instead it becomes easy to make a mistake that leads to results that are difficult to understand and debug.