The latest version will be available as a PR at apple/swift-evolution#786.
Revamp the StrideTo/StrideThrough types
-
Proposal: SE-0199
-
Authors: Max Moiseev, Xiaodi Wu
-
Review Manager: TBD
-
Status: Awaiting implementation
-
Implementation: apple/swift#14288
Introduction
The Swift standard library provides two sequence types StrideTo
and
StrideThrough
, instances of which are returned from stride(from:to:by:)
and stride(from:through:by:)
respectively. These types are designed to be more
flexible versions of Swift ranges, allowing start values less than end values
and stride values other than 1.
Both of these types currently conform to Sequence
. This is quite unfortunate,
because for integer values a StrideTo
instance can conform to
RandomAccessCollection
and provide more performant implementations for methods
like prefix(_:)
, dropFirst(_:)
, etc. That is O(1) for
RandomAccessCollection
as opposed to O(n) for Sequence
.
Motivation
We are going to use StrideTo
type in the motivating examples for brevity, but
the same reasoning applies to StrideThrough
as well.
Let's consider the following snippet:
let myStride = stride(from: 1, to: 10, by: 3)
_ = myStride.dropFirst(1)
The dropFirst(_:)
method call will return a type-erased AnySequence
; that
involves allocating a whole new class instance (although this is an
implementation detail) instead of just advancing the start value by one step.
With the addition of conditional conformances to the language, the result
of zip(_:_:)
can now be a Collection
if both containers being zipped conform
to Collection
. (See
apple/swift#13941 for the details.)
Unfortunately, without StrideTo
conforming to Collection
as proposed,
expressions such as the following do not take advantage of zip
’s conditional
conformance.
zip([1,2,3], stride(from: 2, to: 42, by: 2))
The result of this expression only conforms to Sequence
and as such provides
none of the Collection
APIs.
Proposed solution
We propose to make the following changes to the stride types StrideTo
and
StrideThrough
.
- Make them conform to
RandomAccessCollection
on condition that the
Element.Stride
conforms toBinaryInteger
(see the Detailed design section for the explanation of the condition)
extension StrideTo : RandomAccessCollection
where Element.Stride : BinaryInteger { }
extension StrideThrough : RandomAccessCollection
where Element.Stride : BinaryInteger { }
Since currently both StrideTo
and StrideThrough
define SubSequence
as AnySequence<Element>
, and it only conforms to Sequence
, another modification needs to be made.
- Make both types their own
SubSequence
:
struct StrideTo<Element> {
typealias SubSequence = StrideTo
}
struct StrideThrough<Element> {
typealias SubSequence = StrideThrough
}
We believe these changes will solve the problems outlined in the
Motivation section and make code using results of
stride(from:to:by)
and stride(from:through:by:)
more powerful and more
performant without any modifications to the call sites.
Detailed design
Turning StrideTo
and StrideThrough
into slice types (making them their own
sub-sequences) only requires implementation, not much of a design. Conforming to
the Collection
family of protocols, however, is another topic and is worth
discussing in a little more detail.
Condition for Collection
conformance
There are other Strideable
types in the standard library, including all
floating point number types. However, floating point types represent real
numbers, and striding over even a modest interval may involve a very large
number of iterations if the stride is small. The protocol Collection
, however,
uses Int
as the type of its count
property. So reading the value of count
for an innocuous stride could easily result in trapping on integer overflow if
one is not careful.
This same argument can, of course, be applied to strides of integer values
containing many more elements than an Int
can represent, but we believe this
is a far less frequent situation. Besides, the same behavior can already be
observed with the types provided by the standard library. For example, taking a
distance(from: startIndex, to: endIndex)
of a range -1..<Int.max
will result
in an overflow.
Besides, as mentioned at the very beginning of this proposal, StrideTo
and
StrideThrough
types are more general versions of Swift's ranges, that allow
stride or step other than 1
, and ranges only conform to
RandomAccessCollection
if they are countable, which is expressed as:
extension Range: RandomAccessCollection
where Bound: Strideable, Bound.Stride: SignedInteger { }
The proposed design for StrideTo
and StrideThrough
is therefore:
extension StrideTo : RandomAccessCollection
where Element.Stride : BinaryInteger { }
extension StrideThrough : RandomAccessCollection
where Element.Stride : BinaryInteger { }
Chosing the right Index
type
Another item worth noting is that there are at least 2 possible ways to
represent indices for the new collection types.
- Zero-based
Int
indices - Use
typealias Index = Element
- Use option 1 for
StrideTo
and re-useClosedRange.Index
forStrideThrough
Zero-based indexing is somewhat simpler to implement, but the downside is that
indices of such collections are no longer intechangeable with its sub-sequence's,
which is not the behavior all other standard library collection exhibit. For
example:
let r = 5..<10
let sub = r[7...]
r[sub.startIndex] == sub[sub.startIndex] // results in true
ClosedRange.Index
type is designed to allow for an explicit pastEnd
case
and might be a good candidate for StrideThrough
, but stride-through is
slightly more complicated than a ClosedRange
because the last value can be
jumped over when it does not fall on the stride boundary. This detail will
unnecessarily complicate the implementation of Collection
conformance with no
benefits over option 2.
Therefore, we're left with what seems to be the only option for Index
type:
extension StrideTo: Collection
where Element.Stride: BinaryInteger {
typealias Index = Element
}
extension StrideThrough: Collection
where Element.Stride: BinaryInteger {
typealias Index = Element
}
Source compatibility
This is a source breaking change, as demonstrated by the following example that
would no longer compile.
func consume(_ xs: AnySequence<Int>) { }
consume(stride(from: 1, to: 10, by: 2).dropFirst(1))
However, running the test compatiblity test suite on the branch that implements
this proposal did not reveal any related breakage. We also believe that results
of stride(from:to:by:)
and stride(from:through:by:)
functions are rarely
used in a way that would be incompatible with this change. The majority of the
usages would be to just loop over strides using for..in
or calling Sequence
combinators, both of which are not affected.
Making this change in a backward-compatible way would be much harder and may
result in a worse API overall. (See Alternatives
considered section for details.)
Effect on ABI stability
This work is meant to fix the ABI of the standard library.
Alternatives considered
One other option, and a backward-compatible one in fact, would be to preserve
StrideTo
conformance to Sequence
with the default SubSequence
and
introduce a new type StrideToCollection
unconditionally contrained to
Element.Stride: BinaryInteger
. This solution would also require introducing
new overloads for stride(from:to:by:)
and stride(from:through:by:)
functions, similarly constrained, to produce instances of StrideToCollection
/
StrideThroughCollection
. We believe that this solution results in a less
understandable API that also creates extra work for the type checker.