I asked a poll on Twitter today about API preference between two options (three if you count the updated version):
// the very verbose range-based loop
for n in 0.stride(through: 10, by: 2).reverse() {
print(n)
}
// the more concise range-based loop
for n in 10.stride(through: 0, by: -2) {
print(n)
}
// c-style loop
for var n = 10; n >= 0; n -= 2 {
print(n)
}
And even earlier I wrote this blog article: For Loops and Forced Abstractions.
The primary point of the entry was about being forced into abstractions when they are not necessary.
One of the things that really bothered me were the examples in the Swift blog:
for i in (1...10).reverse() {
print(i)
}
for i in 0.stride(to: 10, by: 2) {
print(i)
}
In my opinion, those are really terrible APIs. In addition being arguably just as bad to visually parse as the c-style for-loop, they still do not convey the intent behind what is being done: they are supposed to be creating a range and only the first usage even comes close to looking like that. Not only that, there is no symmetry involved in incrementing and decrementing ranges.
For example, this is invalid in Swift: 10...0
. So we have, what I would call, a broken and partial abstraction over the concept of “ranges” or “intervals”. Ironically, that’s exactly the API we need, especially when we are removing the c-style for-loop.
Let’s take a look at the Strideable
protocol:
/// Conforming types are notionally continuous, one-dimensional
/// values that can be offset and measured.
public protocol Strideable : Comparable {
/// A type that can represent the distance between two values of `Self`.
associatedtype Stride : SignedNumberType
/// Returns a stride `x` such that `self.advancedBy(x)` approximates
/// `other`.
///
/// - Complexity: O(1).
///
/// - SeeAlso: `RandomAccessIndexType`'s `distanceTo`, which provides a
/// stronger semantic guarantee.
@warn_unused_result
public func distanceTo(other: Self) -> Self.Stride
/// Returns a `Self` `x` such that `self.distanceTo(x)` approximates
/// `n`.
///
/// - Complexity: O(1).
///
/// - SeeAlso: `RandomAccessIndexType`'s `advancedBy`, which
/// provides a stronger semantic guarantee.
@warn_unused_result
public func advancedBy(n: Self.Stride) -> Self
}
This seems fairly clear: it’s an abstraction over an item that can be incremented or decremented by some Self.Stride
value. In addition, we can also determine the distance between two Stridable
instances, so long as they share the same Stride
associated type.
This is one layer of the abstraction onion, but OK. When applied to numeric types, this gives us the nice ability to add and subtract in a generic and type-safe manner.
The problem, in my opinion, is the extension:
extension Strideable {
/// Returns the sequence of values (`self`, `self + stride`, `self +
/// stride + stride`, ... *last*) where *last* is the last value in
/// the progression that is less than `end`.
@warn_unused_result
public func stride(to end: Self, by stride: Self.Stride) -> StrideTo
}
WHAT!?
This makes absolutely no sense to me. I actually find this API really bad on multiple counts:
- Why does a type that is responsible for incrementing itself now have the ability to create a sequence of values?
- What definition of “stride” ever means “create a sequence”?
- The API has a variable named
stride
that has a different conceptual meaning altogether than the function withthe same name.
In my opinion, this is just a bad API. Further, this goes on to confuse matters at the call sites.
If we must get rid of the c-style for loops, then we need to look at what the alternative is: for-in
.
So what is a for-in
loop construct?
You use the for-in loop to iterate over a sequence, such as ranges of numbers, items in an array, or characters in a string.
Source: Swift Programming Language: Control Flow.
Great! So what we really want is the ability to create such a range with as little abstraction as possible. The stride
API is attempting to do that, but it fails to do so in an appropriate matter.
Instead, we want an API that can be called like this:
for n in range(from: 10, to: 0, by: 2) {
}
And here’s what the signature looks like:
func range(
from start: T,
to end: T,
by step: T.Stride = 1) -> Interval
NOTE: Sure, there needs to be other variants to support open, closed, left-open, and right-open intervals, but that’s irrelevant
for this purpose.
Wait a minute… isn’t that the same as what stride()
is today. Sure, except:
range()
is vastly more explicit in what is actually going on.- Instead of tacking on to the
Strideable
protocol like a poor man’s side-car, it composes with it instead creating anAPI that is much more natural and expressive. - Creates a much more natural call site.
I still don’t like the removal for the c-style for-loop, but thankfully, Swift v3 will be moving stride
to be a free function again. It’s nice having a more “proper” API to work with out of the box.
Now to get it renamed to range
…