-
Notifications
You must be signed in to change notification settings - Fork 446
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
Introduce adjacentPairs
#119
Conversation
We have something that can do this already - I wonder if we need a new collection for something we can easily compose: import Algorithms
let numbers = (1...5)
let pairs = numbers.windows(ofCount: 2)
// [[1, 2], [2, 3], [3, 4], [4, 5]] If you wanted to get your exact API for tuples you can write an extension re-using the windows(ofCount:) implementation: extension Collection {
func adjacentPairs() -> AnyCollection<(Element, Element)> {
AnyCollection(windows(ofCount: 2).map {
($0[$0.startIndex], $0[$0.index(after: $0.startIndex)])
})
}
} |
@ollieatkinson While there's overlap between this and |
One of the things we're finding as we build the wrappers in this package is that a collection's For this collection wrapper, that means that we'll need to compute p.s. Thanks so much for bringing this addition to the Algorithms package! |
This is a good call, and something which is worthy to be wary of - when implementing the sliding window algorithm it was one of the primary concerns that access to both It's true that the current implementation of the sliding window algorithm stores the starting offset, I would be interested to know if the impact of that storage is causing trouble for many algorithms given the fantastic capacity of our devices today. I am a little unsure to when we stop writing new wrappers vs using composition of existing types, and improving those to unlock new capabilities - for example, improving the sliding windows algorithms to address any of the concerns we would have to re-use in a implementation of Having said all of this, for what it's worth, the implementation of |
The end user likely won't notice the difference in most scenarios, but in extreme cases it can make a meaningful difference (as described here).
We should certainly compose existing abstractions to make new ones whenever we reasonably can! And |
534881c
to
b0f3020
Compare
@natecook1000 Appreciate the pointers—I've made the following changes:
Let me know if there are any other improvements I can make. |
extension AdjacentPairsCollection { | ||
public typealias Iterator = AdjacentPairsSequence<Base>.Iterator | ||
|
||
@inlinable | ||
public func makeIterator() -> Iterator { | ||
Iterator(base: base.makeIterator()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if this is beneficial, mostly because we're precomputing startIndex
. Won't this mean we end up doing some duplicate work when iterating over someCollection.adjacentPairs()
?
} | ||
|
||
@inlinable | ||
public func index(_ i: Index, offsetBy distance: Int) -> Index { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AdjacentPairsCollection
will also need to implement index(_:offsetBy:limitedBy:)
to properly fulfill the RandomAccessCollection
requirements — the added complexity of a limit could make this quite tricky though, so absolutely feel free to leave it as a TODO or ask for guidance if it's significantly harder than the version without a limit 🙂
adjacentPairs()
also is conceptually similar to windows(ofCount:)
and chunks(ofCount:)
, so their Collection
conformances could be useful to draw inspiration from.
func testIndexDistance() { | ||
let pairSequences = (0...4).map { (0..<$0).adjacentPairs() } | ||
|
||
for pairs in pairSequences { | ||
for index in pairs.indices.dropLast() { | ||
let next = pairs.index(after: index) | ||
XCTAssertEqual(pairs.distance(from: index, to: next), 1) | ||
} | ||
|
||
XCTAssertEqual(pairs.distance(from: pairs.startIndex, to: pairs.endIndex), pairs.count) | ||
XCTAssertEqual(pairs.distance(from: pairs.endIndex, to: pairs.startIndex), -pairs.count) | ||
} | ||
} | ||
|
||
func testIndexOffsetBy() { | ||
let pairSequences = (0...4).map { (0..<$0).adjacentPairs() } | ||
|
||
for pairs in pairSequences { | ||
for index in pairs.indices.dropLast() { | ||
let next = pairs.index(after: index) | ||
XCTAssertEqual(pairs.index(index, offsetBy: 1), next) | ||
} | ||
|
||
XCTAssertEqual(pairs.index(pairs.startIndex, offsetBy: pairs.count), pairs.endIndex) | ||
XCTAssertEqual(pairs.index(pairs.endIndex, offsetBy: -pairs.count), pairs.startIndex) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests are made redundant by the validateIndexTraversals
method that tests whether all index manipulation logic is well-behaved, in an exhaustive manner. Something like this probably covers all edge cases:
func testIndexTraversals() {
validateIndexTraversals(
(0..<0).adjacentPairs(),
(0..<1).adjacentPairs(),
(0..<2).adjacentPairs(),
(0..<3).adjacentPairs(),
(0..<10).adjacentPairs())
}
|
||
extension Collection { | ||
fileprivate static func == <L: Equatable, R: Equatable> (lhs: Self, rhs: Self) -> Bool where Element == (L, R) { | ||
lhs.count == rhs.count && zip(lhs, rhs).allSatisfy(==) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lhs.count == rhs.count && zip(lhs, rhs).allSatisfy(==) | |
lhs.elementsEqual(rhs, by: ==) |
@inlinable | ||
public var endIndex: Index { | ||
switch base.endIndex { | ||
case startIndex.first, startIndex.second: | ||
return startIndex | ||
case let end: | ||
return Index(first: end, second: end) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I strongly suggest unconditionally representing endIndex
as Index(first: base.endIndex, second: base.endIndex)
, and adapting startIndex
to match this representation in the edge case that base.count == 1
(instead of the other way around). This is the approach we take in Windows
as well. Having a consistent representation of endIndex
often makes it easier to reason about index manipulation logic, and I think you'll find that it will improve some of your code.
return i == endIndex | ||
? Index(first: base.index(i.first, offsetBy: distance - 1), | ||
second: base.index(i.first, offsetBy: distance)) | ||
: Index(first: base.index(i.first, offsetBy: distance), | ||
second: i.first) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could avoid computing the first and second indices separately here, since you can cheaply compute the firts if you already have the second.
@inlinable | ||
public static func < (lhs: Index, rhs: Index) -> Bool { | ||
(lhs.first, lhs.second) < (rhs.first, rhs.second) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should only need to compare one of the two underlying indices here instead of both, and the same applies to ==
.
@swift-ci Please test |
Going to merge now and follow up — connected with @mpangburn offline (thank you! 👏) |
Implements an
adjacentPairs
algorithm on Sequence, per forum discussion: https://forums.swift.org/t/add-an-adjacentpairs-algorithm-to-sequence/14817.Description
Lazily iterates over tuples of adjacent elements.
This operation is available for any sequence by calling the
adjacentPairs()
method.Detailed Design
The
adjacentPairs()
method is declared as aSequence
extension returningAdjacentPairs
.The resulting
AdjacentPairs
type is a sequence, with conditional conformance toCollection
,BidirectionalCollection
, andRandomAccessCollection
when the underlying sequence conforms.The spelling
zip(s, s.dropFirst())
for a sequences
is an equivalent operation on collection types; however, this implementation is undefined behavior on single-pass sequences, andZip2Sequence
does not conditionally conform to theCollection
family of protocols.Documentation Plan
Test Plan
Unit tests capture expected index-related invariants about
Collection
and verify expected behavior for sequences of varying lengths, including the empty and single-element sequence cases.Source Impact
This change is strictly additive.
Checklist