[swift-evolution] Feature proposal: Range operator with step

Xiaodi Wu xiaodi.wu at gmail.com
Sun Apr 3 00:17:31 CDT 2016


Milos, you make good points. This thread is really long and hard to
follow, so I'll reply inline below with some observations that have
been made in the past, which I think address some of them. See if you
like where things are headed.

On Sat, Apr 2, 2016 at 8:05 PM, Milos Rankovic via swift-evolution
<swift-evolution at swift.org> wrote:
> `Strideable` types represent an often needed generalisation of `Range` and
> `IntervalType`s. However, `Strideable`’s two `stride` methods are far too
> verbose and unbalanced (in contrast to the natural look and feel of the two
> interval operators). Examples like the following raise a number of issues:
>
>     1.stride(through: 5, by: 2)  // 1, 3, 5
>
>     1.stride(through: 5, by: -2) // []
>
> 1. The method's verbosity keeps the bounds too far apart.
>
> 2. The dot syntax suggests that something is being done to the start bound,
> with the end bound playing the role of an argument, all of which does not
> really reflect the semantics of the call.

An older syntax is being restored in Swift 3: `stride(from: 1, to: 5,
by: 2)` and `stride(from: 1, through: 5, by: 2)`, and dot syntax is
being removed. Bounds are now next to each other, and the start and
end values are now visually equals.

> 3. The direction in which we advance from one end to another of the interval
> is provided twice: once by the order of the bounds and then again by the
> sign of the stride argument.

The stride direction is strictly given by the sign of the last
argument; `stride(from: 1, to: -5, by: 2)` is an empty sequence,
because you cannot get from start to end by -2. See next comment for
why I think this is a feature, not a bug.

> 4. Given the conceptual proximity of `Strideable`, `IntervalType` and
> `Range`, one would expect analogous ways of constructing them.
>
> 5. The word “stride” is not particularly friendly to programmers whose first
> language is not English (again in contrast to the interval operators). This
> is compounded by the distinction between `to` and `through` parameters.
>
> As already noted in this thread, we could simply extend the existing types:
>
>     extension ClosedInterval where Bound : Strideable {
>         func by(stride: Bound.Stride) -> StrideThrough<Bound> {
>             let (s, e) = stride < 0 ? (end, start) : (start, end)
>             return s.stride(through: e, by: stride)
>         }
>     }
>
>     extension HalfOpenInterval where Bound : Strideable {
>         func by(stride: Bound.Stride) -> StrideTo<Bound> {
>             let (s, e) = stride < 0 ? (end, start) : (start, end)
>             return s.stride(to: e, by: stride)
>         }
>     }
>
> So that:
>
>     (1...5).by(2)  // 1, 3, 5
>     (1..<5).by(2)  // 1, 3
>
>     (1...5).by(-2) // 5, 3, 1
>     (1..<5).by(-2) // 5, 3

Yes, I do think that's a great idea, as do other people! Because Dave
A is making some big changes to Range (and Intervals are going away,
leaving only Range), I haven't tried to extend Range in my last
proof-of-concept, but I think there's momentum to add a
`striding(by:)` method to Range to do exactly that, `striding(by:)`
being more clear than `by(_:)`.

One difference between `Range.striding(by:)` and `stride(from:to:by:)`
will be that it's a fatal error to try to construct `1..<(-5)` as a
Range, but if you read the comments in the code for StrideTo, the
original designers of stride explicitly wanted `stride(from: 1, to:
-5, by: 1)` to be allowed. When you can't get from start to end by the
chosen stride, the result is an empty sequence instead of a fatal
error. There may be use cases where that behavior is preferred, so I'm
in favor of adding `striding(by:)` to Range but also keeping
`stride(...)`.

> More exotically, we could make use of subscripts:
>
>     extension ClosedInterval where Bound : Strideable {
>         subscript(stride: Bound.Stride) -> StrideThrough<Bound> {
>             return by(stride)
>         }
>     }
>
>     extension HalfOpenInterval where Bound : Strideable {
>         subscript(stride: Bound.Stride) -> StrideTo<Bound> {
>             return by(stride)
>         }
>     }
>
>     (1...5)[-2] // 5, 3, 1
>
> Or introduce a new, or overload an existing operator, with precedence just
> lower than the two interval operators. For example:
>
>     func > <T> (i: ClosedInterval<T>, stride: T.Stride) -> StrideThrough<T>
> {
>         return i.start.stride(through: i.end, by: stride)
>     }
>
>     func < <T> (i: ClosedInterval<T>, stride: T.Stride) -> StrideThrough<T>
> {
>         return i.end.stride(through: i.start, by: -stride)
>     }
>
>     func > <T> (i: HalfOpenInterval<T>, stride: T.Stride) -> StrideTo<T> {
>         return i.start.stride(to: i.end, by: stride)
>     }
>
>     func < <T> (i: HalfOpenInterval<T>, stride: T.Stride) -> StrideTo<T> {
>         return i.end.stride(to: i.start, by: -stride)
>     }
>
>     for i in 1...5 < 2 {
>         i // 5, 3, 1
>     }
>
>     for i in 1...5 > 2 {
>         i // 1, 3, 5
>     }

I've suggested something like that to be possible earlier in the
thread; didn't get too much of a positive reception. People seem to
like `by(_:)` or `striding(by:)` though.

>
> Not to mention a C-style `for` loop lookalike:
>
>     for i in (1 to 5 by 2) {
>         i // 1, 3, 5
>     }
>
> Obviously, this whole thread is related to the C-style `for` loop (which is
> more general than all of the above solutions) as well as to Haskell-style
> list comprehension syntax (which remains enviable). Nevertheless, I do think
> that a focused, lightweight feature would be the best fit for such a common
> need (just think, for example, how often are such sequences used for
> instructional purposes).
>
> One other possibility is to introduce open-ended, infinite sequences defined
> by a single bound and a stride:
>
>     // infinite sequence, starting with 5 and advancing by -2
>     (5..|-2)
>
> … which could be optionally closed by one of the interval operators:
>
>     (5..|-2)...1
>
> I’ve read somewhere that the “interval is going away”, in which case, a new
> tertiary operator may be worth considering since striding is such a
> fundamental operation. Or really any of the above – just not sticking to the
> existing `stride` methods!
>
> milos
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
>


More information about the swift-evolution mailing list