Some light background
There currently exists 6 types in the standard library that represent intervals.
5 of them conform to RangeExpression
:
Range
ClosedRange
PartialRangeFrom
PartialRangeThrough
PartialRangeUpTo
And a 6th UnboundedRange
that is like a mystery.
My beefs with ranges
-
They do not represent all kinds of intervals. Here are the ones they are able to represent:
let r1 = 0...1 // r1 is [0, 1] let r2 = 0..<1 // r2 is [0, 1) let r3 = 0... // r3 is [0, ∞) let r4 = ...1 // r4 is (-∞, 1] let r5 = ..<1 // r5 is (-∞, 1) let r6 = ... // r6 is (-∞, ∞), I'm not sure if the assignment works tho
These "range" types cover only 6 kinds of intervals.
An interval is defined by its endpoints' values, and the openness of its boundaries. An endpoint can be either bounded (non-infinite) or unbounded (infinite), and a boundary either open or closed. Thus, there are either 8 (if an infinite endpoint cannot have a closed boundary) or 16 (if an infinite endpoint can have a closed boundary) kinds of intervals. No matter which ever rule we use, the "range" types are at least 2 short.
-
You can not easily iterate a
RangeExpression
-conforming type backwards.Since Swift removed C-style for-loops in favour of for-in loops, it has become harder to iterate descendingly than ascendingly. However, sometimes it is convenient and beneficial to do something like this:
for index in 5..>0 { // for-loop block... }
-
All 5 existing
RangeExpression
-conforming types are highly similar in terms of both their use cases and their structures. However, the only thing they share code-wise is theRangeExpression
-conformance. Combined with thatRangeExpression
can only be used as a generic constraint because of its associated typeBound
, it leads to lots of boilerplate code when something needs to work with more than 1 "range" type:For example, starting from line 943 of Range.swift are these 2 functions (with comments omitted):
@inlinable public func overlaps(_ other: Range<Bound>) -> Bool { let isDisjoint = other.upperBound <= self.lowerBound || self.upperBound <= other.lowerBound || self.isEmpty || other.isEmpty return !isDisjoint } @inlinable public func overlaps(_ other: ClosedRange<Bound>) -> Bool { let isDisjoint = other.upperBound < self.lowerBound || self.upperBound <= other.lowerBound || self.isEmpty return !isDisjoint }
Then, starting from line 444 of ClosedRange.swift are 2 more of these functions (again with comments omitted):
@inlinable public func overlaps(_ other: ClosedRange<Bound>) -> Bool { let isDisjoint = other.upperBound < self.lowerBound || self.upperBound < other.lowerBound return !isDisjoint } @inlinable public func overlaps(_ other: Range<Bound>) -> Bool { return other.overlaps(self) }
There will need to be at least 12 more near-identical instance methods to cover all possible overlap tests between "range" types if we ignore polarity (e.g. only
closedRange.overlaps(partialRangeFrom)
, but nopartialRangeFrom.overlaps(closedRange)
) andUnboundedRange
. Then 18 more on top of that if we includeUnboundedRange
, or 33 more if we both include both polarity andUnboundedRange
.In addition to all this, it is even harder to represent an interval that can be either bounded or unbounded or open or closed, using just a variable (or constant). You have to work around it with either a conditional logic or a wrapping type.
For example, starting from line 13 of VersionSetSpecifier.swift in Swift Package Manager is this enum:
/// An abstract definition for a set of versions. public enum VersionSetSpecifier: Hashable { /// The universal set. case any /// The empty set. case empty /// A non-empty range of version. case range(Range<Version>) /// The exact version that is required. case exact(Version) /// A range of disjoint versions (sorted). case ranges([Range<Version>]) }
It needs a custom enum just to work with different kinds of version intervals. If it needs to work with closed intervals, it will have to either add a new case, or convert a
ClosedRange
instance to aRange
instance (and this only works ifBound
isStrideable
). Also,.ranges
only works withRange
. If the set ever needs to contain bothClosedRange
andRange
, then there needs to be another custom type and/or logic. -
Range
is ambiguous. It at least misled me to think that it's a big umbrella over all "range" types. All other "range" types have descriptive names that explain what kind of ranges they are.Range
is probably better calledLeftClosedRightOpenRange
, or something similar. -
UnboundedRange
is a mystery. It works in many places where other "range" types work, but there is no indication of how it works. It's a type alias of (UnboundedRange_
) -> (), which in turn is a complete mystery and has almost no footprint in the entire codebase. The unbounded range operator...
is declared as a postfix operator but acts kind of like an expression. And it's not callable. The only documentation it has (or at least the only bit I can find) is aTODO
: "How does the...
ranges work?"UPDATE: As it turns out,
UnboundedRange
isn't that mysterious after all. See Standard Library thread: How does UnboundedRange work?
My solution
I started a draft pull request that introduces a new generic type Interval
:
https://github.com/apple/swift/pull/32865
This new type should be void of all listed problems above.
It's able to represent all kinds of intervals by holding 4 constants:
public let lowerBoundary: Boundary
public let lowerEndpoint: Endpoint
public let upperEndpoint: Endpoint
public let upperBoundary: Boundary
It can be iterated backwards by using a set of inverse interval operators, or by manually setting a flag:
public var isInverse: Bool
An unbounded interval is not much different from other intervals. It just has unbounded endpoints and open boundaries. It's debatable whether an unbounded interval can be closed. Also, because an unbounded interval is not of a special type, it contains the same wrapped type information as the non-unbounded intervals.
Lots of basic things are already implemented in the draft pull request, but there is still a lot to work on. There are a lot of bikesheddable things too. For example, the interval operators are a bit unwieldy, compared to range operators. And the current Interval
design doesn't allow all the "range" types to become a type alias, like how CountableRange
works.
Overall, I would like to seek the community's opinions and feedbacks, and improve it, before turning it into a proper pitch.