Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Stride for Collection #24

Merged
merged 11 commits into from
Dec 1, 2020

Conversation

ollieatkinson
Copy link
Contributor

@ollieatkinson ollieatkinson commented Oct 14, 2020

Many thanks to @timvermeulen for continued help!

Stride

A type that steps over a collection’s elements by the specified amount.

This is available through the striding(by:) method on any Collection.

(0...10).striding(by: 2) // == [0, 2, 4, 6, 8, 10]

If the stride is larger than the collection count, the resulting wrapper only contains the
first element.

The stride amount must be a positive value.

Detailed Design

The striding(by:) method is declared as a Collection extension, and returns a
Stride type:

extension Collection {
  public func striding(by step: Int) -> Stride<Self>
}

A custom Index type is defined so that it's not possible to get confused when trying
to access an index of the stride collection.

[0, 1, 2, 3, 4].striding(by: 2)[1] // == 1
[0, 1, 2, 3, 4].striding(by: 2).map { $0 }[1] // == 2

A careful thought was given to the composition of these strides by giving a custom
implementation to index(_:offsetBy:limitedBy) which multiplies the offset by the
stride amount.

base.index(i.base, offsetBy: distance * stride, limitedBy: base.endIndex)

The following two lines of code are equivalent, including performance:

(0...10).striding(by: 6)
(0...10).striding(by: 2).stride(by: 3)

Complexity

The call to striding(by: k) is always O(1) and access to the next value in the stride
is O(1) if the collection conforms to RandomAccessCollection, otherwise O(k).

Comparison with other languages

rust has Strided available in a crate.
c++ has std::slice::stride

The semantics of striding described in this documentation are equivalent.


Checklist

  • I've added at least one test that validates that my change is working, if appropriate
  • I've followed the code style of the rest of the project
  • I've read the Contribution Guidelines
  • I've updated the documentation if necessary

@ollieatkinson ollieatkinson marked this pull request as draft October 14, 2020 16:20
@ollieatkinson ollieatkinson marked this pull request as ready for review October 14, 2020 20:30
/// - Parameter step: The amount to step with each iteration.
/// - Returns: Returns a collection stepping through the elements by the
/// specified amount.
public func stride(by step: Int) -> Stride<Self> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this would best be striding to fit the ed/ing rule for naming nonmutating members. That said, I also worry about ambiguity with the existing StrideTo/StrideThrough types: are there other terms that are often used for this operation?

Copy link
Contributor Author

@ollieatkinson ollieatkinson Oct 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am in agreement that striding is probably a better fit for this name for the reasons you've mentioned. I'm a little unsure that renaming the type to something else would be a good idea as the notion of a stride is widely known and something which is obvious as soon as you see it. Other terms used for striding include increment, pitch or step - whilst they offer a similar meaning, they do not have the same understanding as striding. In one of my earlier implementations I had named the type StridingCollection which I would not be against being explicit about, I removed the suffix Collection to keep in step with the rest of the algorithms in this repository. It would be good to see what other feedback I get regarding the naming of this before I change it. Thanks for your thoughts @xwu

Copy link
Member

@natecook1000 natecook1000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good, @ollieatkinson — thanks for this addition!

Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved
Guides/Stride.md Show resolved Hide resolved
Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved
Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved
Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved
Tests/SwiftAlgorithmsTests/StrideTests.swift Outdated Show resolved Hide resolved
Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved
Copy link
Member

@natecook1000 natecook1000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ollieatkinson A few more notes for you!

Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved
Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved
Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved
Tests/SwiftAlgorithmsTests/StrideTests.swift Outdated Show resolved Hide resolved
Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved
Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved

public struct Stride<Base: Collection> {

public let base: Base
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To help simplify the issue, do we need to expose base at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was also unsure of the value of being able to refer to base in this instance, but since all of the other collection algorithms do I thought it would be worth keeping consistent. Was there a specific issue you think that it might simplify?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, the specific issue is the question of equality, which @natecook1000 mentioned above.

Two values are equal when they behave the same way for all salient operations. For this purpose, we say that iteration order isn’t salient for sets but is salient for arrays, that the sign of zero isn’t salient for floating-point numbers, etc.

It’s a judgment call about what is salient here, and reasonable people can disagree whether differing base elements would matter; by contrast, if base isn’t recoverable, then there is no ambiguity at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like what you're saying here and I'd be up for dropping the access modifier from the property - I cannot fathom a situation where you would want to use it outside of the scope of the collection itself.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on this — base can be internal, which removes any controversy here. (That should probably be the case for the rest of the collection wrappers as well.)

Copy link
Contributor Author

@ollieatkinson ollieatkinson Nov 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks both for your thoughts, I've removed public from base here and we can follow up in another P/R for the rest of the collections

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by contrast, if base isn’t recoverable, then there is no ambiguity at all.

I don't see how this matters in case the user knows what base is, even if Stride doesn't expose it. Couldn't there still be controversy about whether (0...10).striding(by: 4) should equal (0...11).striding(by: 4)?

Copy link
Member

@natecook1000 natecook1000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think just these last couple of notes, then this will be ready 👍

}

public func index(_ i: Index, offsetBy distance: Int) -> Index {
precondition(i.base < base.endIndex, "Advancing past end index")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a valid precondition, since c.index(c.endIndex, offsetBy: -1) is valid for bidirectional collections.

Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved
@ollieatkinson
Copy link
Contributor Author

I've rebased against the latest main and included some test cases using the new validateIndexTraversals TestUtilities API. Before merging, it would be good to hear your thoughts on the points that xwu raised here

Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved
Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved
Tests/SwiftAlgorithmsTests/StrideTests.swift Outdated Show resolved Hide resolved
Tests/SwiftAlgorithmsTests/StrideTests.swift Outdated Show resolved Hide resolved
Tests/SwiftAlgorithmsTests/StrideTests.swift Show resolved Hide resolved
@timvermeulen
Copy link
Contributor

Stride probably shouldn't require that Base: Collection, but instead implement only Sequence when Base: Sequence.

@ollieatkinson
Copy link
Contributor Author

ollieatkinson commented Nov 6, 2020

@timvermeulen

Stride probably shouldn't require that Base: Collection, but instead implement only Sequence when Base: Sequence.

That makes sense, I've added a commit to address this behaviour here

Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved
Sources/Algorithms/Stride.swift Outdated Show resolved Hide resolved
@ollieatkinson
Copy link
Contributor Author

@natecook1000 Is there any more work required on this for it to be merged?

limitedBy limit: Index
) -> Index? {
let distance = i == endIndex
? -((base.count - 1) % stride + 1) + (n - 1) * -stride
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, Xcode 12.2 fails to compile this line due to an "expression too complex" error. Making the 1 values Int(1) seems to resolve the issue.

Copy link
Contributor Author

@ollieatkinson ollieatkinson Nov 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for raising this - I'm surprised that it's too complex given these are all integers, I could also annotate the distance property : Int to have the same effect.

I have just tested this on Big Sur 11.0.1 and Xcode 12.2 and I was able to compile fine - do you have anything else in your environment which might cause this, so I can track it down?

@natecook1000
Copy link
Member

Thanks, @ollieatkinson — this is ready to go!

@natecook1000 natecook1000 merged commit ff01a02 into apple:main Dec 1, 2020
@ollieatkinson ollieatkinson deleted the striding-collection branch December 2, 2020 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants