Range
conforms to Sequence
under the condition Bound: Strideable, Bound.Stride: SignedInteger
.
However, BinaryInteger
does not currently provide SignedInteger
conformance for its Stride
.
This causes problems in generic code. Specifically, when looping over a generic integer type, we cannot use a for
loop:
func foo<T: BinaryInteger>(_ a: T, _ b: T) {
for i in a ..< b { // error: Operator function '..<' requires that 'T.Stride' conform to 'SignedInteger'
print(i)
}
}
Note that we can form the range itself: let range = a ..< b
. We just can’t iterate over it or perform other sequence / collection operations like shuffled()
, first(where:)
, and randomElement()
.
Furthermore, even changing the generic constraint from T: BinaryInteger
to T: SignedInteger
does not fix the problem. It is still the same error for signed integers.
Of course a while
loop works fine even with BinaryInteger
, which highlights that logically there is no reason to prohibit iteration here:
func foo<T: BinaryInteger>(_ a: T, _ b: T) {
precondition(a <= b)
var i = a
while i < b {
print(i)
i += 1
}
}
• • •
Personally, I just encountered this issue while writing some generic floating-point code where I needed to iterate through a range of exponents.
Having to write a while
loop rather than a for
loop is not a major showstopper, just a minor annoyance. But it is still an annoyance, and in a language like Swift that takes pride in its clarity, a for
loop over a range is much more clear to readers than a while
loop with manual increments.
In another situation, instead of a simple loop, one might want to call a generic algorithm on the range such as sorted(by:)
. And that being disallowed is a much larger annoyance.
• • •
I know this has been mentioned on the forums before, such as in the thread, Why does a for loop range need a signed integer?, but I don’t recall any discussion about actually rectifying the situation.
Currently:
• BinaryInteger
is declared as being Strideable
.
• Strideable.Stride
is declared as being SignedNumeric
.
So it follows that BinaryInteger.Stride
is always SignedNumeric
.
But today there is no constraint that BinaryInteger.Stride
must be an integer.
• • •
I think it is completely reasonable to require that the stride of an integer should itself be an integer. And, since strides are always signed, it follows that the stride of an integer should really be a signed integer.
Thus, the missing piece is that BinaryInteger.Stride
needs to conform to SignedInteger
.
Specifically, the BinaryInteger
protocol should be declared with a constraint where Stride: SignedInteger
.
Note that FixedWidthInteger
already has that constraint. Adding it to BinaryInteger
would allow ranges of any generic integer type to be used as collections, including for
loops and calling generic algorithms.
• • •
Conceptually for users this change is trivial: every integer type should already have integer strides, and strides are always signed.
What would an integer type with non-integer strides even mean? It doesn’t make sense as a concept.
The only place this would break source-compatibility is on the declaration of an integer type with non-integer strides, which should not exist in the first place.
• • •
However, I suspect there might be binary compatibility concerns with adding a constraint to a standard-library protocol. I don’t know enough about the ABI to say for sure, but it seems like the sort of thing that might cause issues.
So I wanted to gauge whether this (adding where Stride: SignedInteger
to the declaration of BinaryInteger
) is something that could be done in a normal release, almost like a bug-fix, or if it would need to wait for a full version change like Swift 6.