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.