[swift-evolution] [Pitch] Change the endIndex value for closed Ranges and Collections
Pedro Vieira
pedrovieira.swift at gmail.com
Wed Mar 23 12:17:52 CDT 2016
Hi Swift developers,
I hereby present my first Swift proposal regarding the endIndex on
`Collections` and closed `Ranges`. I’ve searched the mailing list archives
for something similar to this but couldn’t find it, so I decided to come
forward.
*The problem:*
I was recently working on a library and used a closed Range to define the
bounds of a board game and the tests that used its endIndex were all
failing. Started debugging it around and, to my complete surprise, found
out that the endIndex of the closed Ranges was always +1 from the value I
initially set. With this, I decided to dive into the Range source code and
discovered that all closed ranges are converted to half-open ones when
initialized:
a) 1..<10 stays 1..<10 (endIndex = 10)
b) 1...10 is converted to 1..<11 (endIndex = 11)
To work around this behavior I had to subtract 1 every time I checked for
the endIndex, since I didn't want to use last! (force unwrapping) or
if-let, which ultimately polluted my code. You could argue that changing
from 1...10 to 1...9 would get rid of all of this, since it gets translated
to 1..<10 with endIndex being 10 (which is the value I expect), but I think
it’s just not worth it to “obscure” that way.
I’ve asked some fellow developer friends their thoughts having the value 11
when accessing endIndex on b) and a lot of them were confused and did not
expect the outcome, and I totally agree, it’s not intuitive at all. To me,
endIndex implies that the returned value is the last valid and accessible
index of a Collection, not its size/count or any other value that is
outside the collection’s bounds.
*The solution:*
Add an optional boolean parameter, `closed`, to the Range init method with
the default value of false (instead of `closed` there’s always the
`halfOpen` alternative):
init(_start: Element, end: Element, closed: Bool = false)
The parameter is an optional parameter to minimize the impact on existing
code that is currently initializing Ranges using the init method directly.
This parameter would also be a public property.
Then, the ... constructor would become:
public func ... <Pos : ForwardIndex> (minimum: Pos, maximum: Pos) -> Range<
Pos> {
- return Range(_start: minimum, end: maximum.successor())
+ return Range(_start: minimum, end: maximum, closed: true)
}
Also, the next() method from the RangeGenerator would become:
public mutating func next() -> Element? {
- if startIndex == endIndex.successor() { return nil }
+ if startIndex == (isClosed ? endIndex.successor() : endIndex) { return
nil }
let element = startIndex
startIndex._successorInPlace()
return element
}
Performing this change has 2 main benefits:
1 — The Range description and debugDescription properties would finally
return a *true* self String representation. So, for instance, the
description property would become:
public var description: String {
- return "\(startIndex)..<\(endIndex)"
+ return "\(startIndex)..\(isClosed ? "." : "<")\(endIndex)"
}
2 — It becomes a lot more intuitive. WYSIWYG. Also, by having the `closed`
parameter in the init method, developers that create Ranges directly from
the init method will actually know what type of Range they’re getting
straight away:
- currently: Range(start: 1, end: 10) ==> is it 1...10 or 1..<10?
- proposed: Range(start: 1, end: 10, closed: true) ==> 1...10
This behavior is also present in Swift Collections:
let array = [5, 10, 15]
array.count // 3
array.endIndex // 3 (should be 2)
array.last! // 15 (apologies for the force unwrap 🤓)
Again, the endIndex returns an index that is outside the array bounds,
which, in fact, acts as the array count. Instead, it should have returned
the index of the last element.
I know that, in the comments, it’s explicit: “A 'past-the-end' element
index; the successor of the last valid subscript argument.”, but, in the
end, it all comes down to readability.
What are your thoughts on this? Let me know.
Cheers,
--
Pedro Vieira
http://pedrovieira.me
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160323/bed3412c/attachment.html>
More information about the swift-evolution
mailing list