Some more directions and issues to consider.
The RangeExpression conformance issue
RangeExpression requires contains(_:Bound) -> Bool, which we can only answer partially if there is an existing index and not at all if there are only pure offsets. In hindsight, these should have been separate concepts, but they’re not so we’re stuck with it.
One option is to “fake” conformance by having contains return false or provide a partial order (as in this pitch). The thought is that relative(to:) is far more likely to be used by code in the wild. Unfortunately, there is some code in the wild which calls contains.
Another option would be to trap inside contains so at least it will be caught rather than give the wrong answer.
Another is to not conform to RangeExpression. Conformance allows us to avoid adding an extra subscript overload (as well as replaceSubrange and removeSubrange if we care enough to do so). We could instead add another concrete overload for RelativeRange or make a new less-constrained IndexRangeExpression protocol. E.g.
protocol IndexRangeExpression {
associatedtype Index // : Comparable (maybe) because `relative` requires it...
func relative<C: Collection>(to: C) -> Range<C.Index> where Index == C.Index
}
The same-type constraint on Index would force OffsetRange, discussed below, to continue down the phantom-type path.
Deprecating RangeExpression at this point is a little tough as there are many conformers in the wild. We do not have compiler support currently to insert IndexRangeExpression as a parent of RangeExpression, but we could do that in the future whenever supports lands.
OffsetRange
This is @xwu’s suggestion. This would represent offsets from the start or end of a collection but not from a given index. While this loses the flexibility of offsetting an existing index, it gains the ability to be used as a currency type applicable to multiple collections, which Range<Index> is not. On the other hand, such a thing doesn’t represent much functionality and could be replaced with e.g. a closure that slices a collection.
It requires an OffsetBound type as well. If this replaces RelativeRange and RelativeBound, it is a simplification of this pitch. Otherwise, it is a pair of new types in addition to this pitch.
This has the same conformance issues with respect to RangeExpression, just s/Relative/Offset. If this conforms to RangeExpression or some IndexRangeExpression, OffsetBound and OffsetRange will need to be phantom-typed, limiting their applicability to those collections with the same index type it was defined for:
public enum OffsetBound<👻> {
case fromStart(Int)
case fromEnd(Int)
}
public struct OffsetRange<👻>: IndexRangeExpression {
var lowerBound: OffsetBound<👻>
var upperBound: OffsetBound<👻>
}
If we go the route of no protocols, that is just concrete overloads for subscript (and others if we care), then OffsetRange can lose the phantom type and be a little more general. In exchange, we have more overloads.
Bikeshedding Operators
++/-- are understandably controversial. I gave a prototype of using symmetric --> and <--, which can be substituted with any desired symmetric pair. Let there be bikeshedding.
Faking Syntax
This whole area of ranges is faking proper language syntax. We use operators, which necessitate a gaggle of types such as PartialRangeUpTo, RelativeBound, etc., which are needed for intermediary results. Really, there should be Range and everything else is a just means to get to Range. We don’t have infix operators with implicit arguments, so we fake them with prefix/postfix operators. Prefix/postfix operators do not participate in infix operator precedence, so we fake it by adding yet more overloads. The no-fix ... operator is its own special thing. Further, we are limited in syntax for offsets by what general purpose operators are available (e.g. no ->). These put more stress on the type checker than would be necessary with proper language syntax for range expressions.
An alternative is to do proper language syntax for range expressions. This would be an open syntax design area. It can be library-extensible through an ExpressibleByRangeExpression protocol, which is invoked with the subexpressions and produces a Range (somewhat analogous to how ExpressibleByStringInterpolation is invoked over a list of segments).
RangeExpression and anything we add from this pitch would be obsoleted by the new syntax and approach.
Practically speaking, unless a very motivated individual steps up to drive this, choosing to wait for language syntax is likely postponing progress here by years.
As for potential for type checker issues, @xedin, how can we evaluate the new overloads in this pitch? I was careful to avoid overlap, but how should we think about the impact of these?