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
_______________________________________________ swift-evolution mailing list swift-evolution@swift.org https://lists.swift.org/mailman/listinfo/swift-evolution