Skip to content

Instantly share code, notes, and snippets.

@erica
Last active April 8, 2016 18:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erica/a51a981ee0352235204692affa959307 to your computer and use it in GitHub Desktop.
Save erica/a51a981ee0352235204692affa959307 to your computer and use it in GitHub Desktop.

Introducing a striding(by:) method on 3.0 ranges

Introduction

We propose to introduce a striding(by:) method on the revised 3.0 Range type.

This proposal was discussed on the Swift Evolution list in the Feature proposal: Range operator with step thread. (Direct link to original thread)

Motivation

Updating Range for Swift 3 offers a window of opportunity to simultaneously improve strides.

  • Under current Swift 3 plans, n.stride(to:/through:, by:) will be replaced with a standalone stride(from:, to:/through:, by:) function. We propose to replace this change with a method on ranges. Using a method reduces overall API surface area compared to free functions.

  • In its current incarnation, the standalone stride function uses confusing semantics. The current to implementation returns values in [start, end) and will never reach or get to end. The current through implementation returns values in [start, end]. It may never reach end and certainly never goes through that value. Our proposed method introduces simple, expected semantics that can be extended to both countable and continuous ranges, and to open and closed intervals (both half-open and fully-open).

Detail Design

The striding(by:) method is called on ranges. When used with a positive step size, the count starts from the lower bound. With a negative step size, the count starts from the upper bound. These bounds apply regardless of whether they are inclusive or exclusive.

The following examples should cover all corner cases and include possible cases should Swift 3 introduce a full complement of open and closed ranges. The syntax for non-canonical range types is not fixed and can be discussed under separate cover.

(0 ... 9).striding(by: 2) == [0, 2, 4, 6, 8]
(0 ..< 9).striding(by: 2) == [0, 2, 4, 6, 8]
(0 <.. 9).striding(by: 2) ==    [2, 4, 6, 8]
(0 <.< 9).striding(by: 2) ==    [2, 4, 6, 8]

(0 ... 9).striding(by: 3) == [0, 3, 6, 9]
(0 ..< 9).striding(by: 3) == [0, 3, 6]
(0 <.. 9).striding(by: 3) ==    [3, 6, 9]
(0 <.< 9).striding(by: 3) ==    [3, 6]

(0 ... 9).striding(by: -2) == [9, 7, 5, 3, 1]
(0 ..< 9).striding(by: -2) ==    [7, 5, 3, 1]
(0 <.. 9).striding(by: -2) == [9, 7, 5, 3, 1]
(0 <.< 9).striding(by: -2) ==    [7, 5, 3, 1]

(0 ... 9).striding(by: -3) == [9, 6, 3, 0]
(0 ..< 9).striding(by: -3) ==    [6, 3, 0]
(0 <.. 9).striding(by: -3) == [9, 6, 3]
(0 <.< 9).striding(by: -3) ==    [6, 3]

To reverse a stride, call reverse() on the results:

(0 ... 9).striding(by: 2).reverse() == [8, 6, 4, 2, 0]

We note that striding by 0 should be always be a precondition failure.

Alternatives Considered

During the on-list discussion, we considered various scenarios that took closed/inclusive bounds into account or excluded open bounds for starting values. For example, we might have prohibited scenarios where multiple interpretations of an intended behavior might exist: is (0 ..< 9).striding(by: -2) a precondition failure? We settled on the simplest, most straight-forward implementation involving the fewest compiler warnings and the lowest likelihood of precondition failures. We subscribe to the "Dave Abrahams Philosophy": excessive special casing and warning scenarios more likely indicates bad language design than bad user comprehension.

Future Directions

We intend to follow up with an expanded operator vocabulary that includes fully open ranges (<.<), fully closed ranges (...) and both half open ranges (<.., ..<). These will support the full vocabulary laid out in the Detail Design section.

Upon adoption, the Swift community may consider expanding this approach to collection indices, for example:

let a = [8, 6, 7, 5, 3, 0, 9]
for e in a.striding(by: 3) {
    print(e) // 8, then 5, then 9
}

Striding offers a fundamental operation over collections. The consistent approach introduced in this proposal helps support the extension of stride semantics to collections.

Acknowlegements

Thanks, Dave Abrahams, Matthew Judge

@xwu
Copy link

xwu commented Apr 7, 2016

One alternative to consider (suggested by Matthew Judge):

  • Limit striding(by:) to closed/inclusive bounds only. Thus, prohibit the only scenario where multiple interpretations of the intended behavior might exist, so that (0 ..< 9).striding(by: -2) is a precondition failure, and likewise (0 <.. 9).striding(by: 2). Should an open range (<.<) be implemented, this design precludes striding over such a range.

@natecook1000
Copy link

Another idea: Why limit this to ranges only? It seems like this API would be equally useful on any collection and keep the current preference for iterating over a collections elements instead iterating over indices and using the indices to access elements.

let a = [8, 6, 7, 5, 3, 0, 9]
for e in a.striding(by: 3) {
    print(e)
}
// 8
// 5
// 9

@pyrtsa
Copy link

pyrtsa commented Apr 7, 2016

s/To reverse a positive stride/To reverse a stride/

@pyrtsa
Copy link

pyrtsa commented Apr 7, 2016

@natecook1000 I like that thinking. Swift-the-language is still missing conditional protocol conformance but I think the idea is to make all countable ranges (e.g. open or closed range of Int) behave as collections. By implementing .striding(by:) as part of forward and bidirectional collections, this proposal would naturally result from that implementation.

But maybe it's better to stick to the narrower functionality at first. Moving the implementation into the protocol would be compatible anyway.

@dabrahams
Copy link

I agree with @natecook as noted in this post

@erica
Copy link
Author

erica commented Apr 7, 2016

@dabrahams I put @natecook1000's point in future directions. Do you want me to move it into motivation?

@dabrahams
Copy link

Took me a few days to get this clear:

yes, we can do this for collections too but it doesn’t solve the problem of striding over floating point, as floating point ranges are not collections.

So, future directions sounds good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment