Extract the ClosedRange.Index type to top-level

Motivation

The ClosedRange.Index type has proven to be useful. Any collection indexable by a bounded type has to represent an endIndex and, at the same time, allow the maximum value of the index type to be valid for subscripting (this is a long version of saying "collection c indexable by Int should allow c[Int.max]).

Detailed design

Currently there are two implementations of the same enum: one is ClosedRange.Index itself, and second is inside the LazyPrefixWhileCollection. There is also an attempt to make StrideThrough conform to Collection, and this same technique will have to be applied there as well.

The reason why this type needs to be extracted from inside the ClosedRange (as opposed to just be reused as is) is because ClosedRange imposes a Strideable constraint on its Bound associated type, which is not required, and in fact is very limiting, for the Index type.

Proposed solution

I propose promoting the type now known as ClosedRange.Index to the top level, and giving it a new name of PastEndIndex. Name is up for debate, obviously,

Implementation

The implementation is available in apple/swift#15193.

5 Likes

Cant see why Index? couldn't be used to express the same concept just as succinctly and it would read well, with .none or nil meaning not a valid index (in this specific case past end).

@hlovatt, Optional already has suitable Equatable and Hashable conditional conformances.

But you'd need to add a Comparable conditional conformance, in which .some(bound) < .none is always true. This might be surprising in general use, and different to other languages (e.g. C++17 std::optional).

1 Like

Yes you raise a good point.

If you only have two states then you have, in effect, valid and invalid. How do you compare valid to invalid? I would say it's like comparing a float to NaN; all the comparisons should be false. You can't get this exact behaviour but you can get close:

extension Optional: Comparable where Wrapped: Comparable {
    public static func < (lhs: Optional<Wrapped>, rhs: Optional<Wrapped>) -> Bool {
        guard let lValue = lhs, let rValue = rhs else {
            return false
        }
        return lValue < rValue
    }
}

let xo: Int? = 0
let yo: Int? = nil

xo == xo // T
xo < xo // F
xo > xo // F

yo == yo // T - would ideally like F :(, but probably OK
yo < yo // F
yo > yo // F

xo == yo // F
xo < yo // F
xo > yo // F

@hlovatt, if your original suggestion was to use ClosedRange.Index = Bound? then:

let closedRange = UInt8.min ... UInt8.max

closedRange.lowerBound // $R0: UInt8 = 0
closedRange.upperBound // $R1: UInt8 = 255

closedRange.startIndex // $R2: UInt8? = 0
closedRange.endIndex   // $R3: UInt8? = nil

// Should the following be true or false...
closedRange.startIndex < closedRange.endIndex

I think a separate PastEndIndex<Bound> enum is better than using the Optional enum.

3 Likes

I'd like to highlight that this functionality (not through conditional conformances though) was in Swift before, and was explicitly removed.