Skip to content

Rename chained(with:) to chain(_:_:) #33

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

Merged
merged 2 commits into from
Oct 23, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 20 additions & 14 deletions Guides/Chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@

Concatenates two collections with the same element type, one after another.

This operation is available through the `chained(with:)` method on any sequence.
This operation is available for any two sequences by calling the `chain(_:_:)`
function.

```swift
let numbers = [10, 20, 30].chained(with: 1...5)
let numbers = chain([10, 20, 30], 1...5)
// Array(numbers) == [10, 20, 30, 1, 2, 3, 4, 5]
//
let letters = "abcde".chained(with: "FGHIJ")

let letters = chain("abcde", "FGHIJ")
// String(letters) == "abcdeFGHIJ"
```

Expand All @@ -21,25 +22,30 @@ the shared conformances of the two underlying types.

## Detailed Design

The `chained(with:)` method is added as an extension method on the `Sequence`
protocol:
The `chain(_:_:)` function takes two sequences as arguments:

```swift
extension Sequence {
public func chained<S: Sequence>(with other: S) -> Concatenation<Self, S>
where Element == S.Element
}
public func chain<S1, S2>(_ s1: S1, _ s2: S2) -> Chain2<S1, S2>
where S1.Element == S2.Element
```

The resulting `Chain` type is a sequence, with conditional conformance to
The resulting `Chain2` type is a sequence, with conditional conformance to
`Collection`, `BidirectionalCollection`, and `RandomAccessCollection` when both
the first and second arguments conform. `Chain` also conforms to
the first and second arguments conform. `Chain2` also conforms to
`LazySequenceProtocol` when the first argument conforms.

### Naming

This method’s and type’s name match the term of art used in other languages and
libraries.
This function's and type's name match the term of art used in other languages
and libraries.

This operation was previously implemented as a `Sequence` method named
`chained(with:)`, and was switched to a free function to align with APIs like
`zip` and `product` after [a lengthy forum discussion][naming]. Alternative
suggestions for method names include `appending(contentsOf:)`, `followed(by:)`,
and `concatenated(to:)`.

[naming]: https://forums.swift.org/t/naming-of-chained-with/40999/

### Comparison with other languages

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Read more about the package, and the intent behind it, in the [announcement on s

#### Combining collections

- [`chained(with:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Chain.md): Concatenates two collections with the same element type.
- [`chain(_:_:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Chain.md): Concatenates two collections with the same element type.
- [`product(_:_:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Product.md): Iterates over all the pairs of two collections; equivalent to nested `for`-`in` loops.
- [`cycled()`, `cycled(times:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Cycle.md): Repeats the elements of a collection forever or a set number of times.

Expand Down
84 changes: 48 additions & 36 deletions Sources/Algorithms/Chain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//===----------------------------------------------------------------------===//

/// A concatenation of two sequences with the same element type.
public struct Chain<Base1: Sequence, Base2: Sequence>
public struct Chain2<Base1: Sequence, Base2: Sequence>
where Base1.Element == Base2.Element
{
/// The first sequence in this chain.
Expand All @@ -25,7 +25,7 @@ public struct Chain<Base1: Sequence, Base2: Sequence>
}
}

extension Chain: Sequence {
extension Chain2: Sequence {
/// The iterator for a `Chain` sequence.
public struct Iterator: IteratorProtocol {
@usableFromInline
Expand All @@ -35,7 +35,7 @@ extension Chain: Sequence {
internal var iterator2: Base2.Iterator

@usableFromInline
internal init(_ concatenation: Chain) {
internal init(_ concatenation: Chain2) {
iterator1 = concatenation.base1.makeIterator()
iterator2 = concatenation.base2.makeIterator()
}
Expand All @@ -52,7 +52,7 @@ extension Chain: Sequence {
}
}

extension Chain: Collection where Base1: Collection, Base2: Collection {
extension Chain2: Collection where Base1: Collection, Base2: Collection {
/// A position in a `Chain` collection.
public struct Index: Comparable {
// The internal index representation, which can either be an index of the
Expand Down Expand Up @@ -253,7 +253,7 @@ extension Chain: Collection where Base1: Collection, Base2: Collection {
}
}

extension Chain: BidirectionalCollection
extension Chain2: BidirectionalCollection
where Base1: BidirectionalCollection, Base2: BidirectionalCollection
{
@inlinable
Expand All @@ -270,45 +270,57 @@ extension Chain: BidirectionalCollection
}
}

extension Chain: RandomAccessCollection
extension Chain2: RandomAccessCollection
where Base1: RandomAccessCollection, Base2: RandomAccessCollection {}
extension Chain: LazySequenceProtocol where Base1: LazySequenceProtocol {}
extension Chain2: LazySequenceProtocol where Base1: LazySequenceProtocol {}
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this conformance should be removed altogether with chain being a free function.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes! Another wrinkle in the various keep-it-lazy shenanigans.


extension Chain: Equatable where Base1: Equatable, Base2: Equatable {}
extension Chain: Hashable where Base1: Hashable, Base2: Hashable {}
extension Chain2: Equatable where Base1: Equatable, Base2: Equatable {}
extension Chain2: Hashable where Base1: Hashable, Base2: Hashable {}

//===----------------------------------------------------------------------===//
// chained(with:)
// chain(_:_:)
//===----------------------------------------------------------------------===//

/// Returns a new sequence that iterates over the two given sequences, one
/// followed by the other.
///
/// You can pass any two sequences or collections that have the same element
/// type as this sequence. This example chains a closed range of `Int` with an
/// array of `Int`:
///
/// let small = 1...3
/// let big = [100, 200, 300]
/// for num in chain(small, big) {
/// print(num)
/// }
/// // 1
/// // 2
/// // 3
/// // 100
/// // 200
/// // 300
///
/// - Parameters:
/// - s1: The first sequence.
/// - s2: The second sequence.
/// - Returns: A sequence that iterates first over the elements of `s1`, and
/// then over the elements of `s2`.
///
/// - Complexity: O(1)
public func chain<S1, S2>(_ s1: S1, _ s2: S2) -> Chain2<S1, S2> {
Chain2(base1: s1, base2: s2)
}

// MARK: - Deprecations

@available(*, deprecated, renamed: "Chain2")
public typealias Chain = Chain2

extension Sequence {
/// Returns a new sequence that iterates over this sequence, followed by the
/// given sequence.
///
/// You can pass a sequence or collection of any type that has the same
/// element type as this sequence. This example chains a closed range of `Int`
/// with an array of `Int`:
///
/// let small = 1...3
/// let big = [100, 200, 300]
/// for num in small.chained(with: big) {
/// print(num)
/// }
/// // 1
/// // 2
/// // 3
/// // 100
/// // 200
/// // 300
///
/// - Parameter other: The sequence to iterate over after this sequence.
/// - Returns: A sequences that follows iteration of this sequence with
/// `other`.
///
/// - Complexity: O(1)
public func chained<S: Sequence>(with other: S) -> Chain<Self, S>
@available(*, deprecated, message: "Use the chain(_:_:) function, instead.")
public func chained<S: Sequence>(with other: S) -> Chain2<Self, S>
where Element == S.Element
{
Chain(base1: self, base2: other)
Chain2(base1: self, base2: other)
}
}
66 changes: 33 additions & 33 deletions Tests/SwiftAlgorithmsTests/ChainTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,78 +15,78 @@ import XCTest
final class ChainTests: XCTestCase {
// intentionally does not depend on `Chain.index(_:offsetBy:)` in order to
// avoid making assumptions about the code being tested
func index<A, B>(atOffset offset: Int, in chain: Chain<A, B>) -> Chain<A, B>.Index {
func index<A, B>(atOffset offset: Int, in chain: Chain2<A, B>) -> Chain2<A, B>.Index {
offset < chain.base1.count
? .init(first: chain.base1.index(chain.base1.startIndex, offsetBy: offset))
: .init(second: chain.base2.index(chain.base2.startIndex, offsetBy: offset - chain.base1.count))
}

func testChainSequences() {
let run = (1...).prefix(10).chained(with: 20...)
let run = chain((1...).prefix(10), 20...)
XCTAssertEqualSequences(run.prefix(20), Array(1...10) + (20..<30))
}

func testChainForwardCollection() {
let s1 = Set(0...10)
let s2 = Set(20...30)
let c = s1.chained(with: s2)
let c = chain(s1, s2)
XCTAssertEqualSequences(c, Array(s1) + Array(s2))
}

func testChainBidirectionalCollection() {
let s1 = "ABCDEFGHIJ"
let s2 = "klmnopqrstuv"
let c = s1.chained(with: s2)
let c = chain(s1, s2)

XCTAssertEqualSequences(c, "ABCDEFGHIJklmnopqrstuv")
XCTAssertEqualSequences(c.reversed(), "ABCDEFGHIJklmnopqrstuv".reversed())
XCTAssertEqualSequences(s1.reversed().chained(with: s2), "JIHGFEDCBAklmnopqrstuv")
XCTAssertEqualSequences(chain(s1.reversed(), s2), "JIHGFEDCBAklmnopqrstuv")
}

func testChainIndexOffsetBy() {
let s1 = "abcde"
let s2 = "VWXYZ"
let chain = s1.chained(with: s2)
let c = chain(s1, s2)

for (startOffset, endOffset) in product(0...chain.count, 0...chain.count) {
let start = index(atOffset: startOffset, in: chain)
let end = index(atOffset: endOffset, in: chain)
for (startOffset, endOffset) in product(0...c.count, 0...c.count) {
let start = index(atOffset: startOffset, in: c)
let end = index(atOffset: endOffset, in: c)
let distance = endOffset - startOffset
XCTAssertEqual(chain.index(start, offsetBy: distance), end)
XCTAssertEqual(c.index(start, offsetBy: distance), end)
}
}

func testChainIndexOffsetByLimitedBy() {
let s1 = "abcd"
let s2 = "XYZ"
let chain = s1.chained(with: s2)
let c = chain(s1, s2)

for (startOffset, limitOffset) in product(0...chain.count, 0...chain.count) {
let start = index(atOffset: startOffset, in: chain)
let limit = index(atOffset: limitOffset, in: chain)
for (startOffset, limitOffset) in product(0...c.count, 0...c.count) {
let start = index(atOffset: startOffset, in: c)
let limit = index(atOffset: limitOffset, in: c)

// verifies that the target index corresponding to each offset in `range`
// can or cannot be reached from `start` using
// `chain.index(start, offsetBy: _, limitedBy: limit)`, depending on the
// `c.index(start, offsetBy: _, limitedBy: limit)`, depending on the
// value of `beyondLimit`
func checkTargetRange(_ range: ClosedRange<Int>, beyondLimit: Bool) {
for targetOffset in range {
let distance = targetOffset - startOffset

XCTAssertEqual(
chain.index(start, offsetBy: distance, limitedBy: limit),
beyondLimit ? nil : index(atOffset: targetOffset, in: chain))
c.index(start, offsetBy: distance, limitedBy: limit),
beyondLimit ? nil : index(atOffset: targetOffset, in: c))
}
}

// forward
if limit >= start {
// the limit has an effect
checkTargetRange(startOffset...limitOffset, beyondLimit: false)
checkTargetRange((limitOffset + 1)...(chain.count + 1), beyondLimit: true)
checkTargetRange((limitOffset + 1)...(c.count + 1), beyondLimit: true)
} else {
// the limit has no effect
checkTargetRange(startOffset...chain.count, beyondLimit: false)
checkTargetRange(startOffset...c.count, beyondLimit: false)
}

// backward
Expand All @@ -102,42 +102,42 @@ final class ChainTests: XCTestCase {
}

func testChainIndexOffsetAcrossBoundary() {
let chain = "abc".chained(with: "XYZ")
let c = chain("abc", "XYZ")

do {
let i = chain.index(chain.startIndex, offsetBy: 3, limitedBy: chain.startIndex)
let i = c.index(c.startIndex, offsetBy: 3, limitedBy: c.startIndex)
XCTAssertNil(i)
}

do {
let i = chain.index(chain.startIndex, offsetBy: 4)
let j = chain.index(i, offsetBy: -2)
XCTAssertEqual(chain[j], "c")
let i = c.index(c.startIndex, offsetBy: 4)
let j = c.index(i, offsetBy: -2)
XCTAssertEqual(c[j], "c")
}

do {
let i = chain.index(chain.startIndex, offsetBy: 3)
let j = chain.index(i, offsetBy: -1, limitedBy: i)
let i = c.index(c.startIndex, offsetBy: 3)
let j = c.index(i, offsetBy: -1, limitedBy: i)
XCTAssertNil(j)
}
}

func testChainDistanceFromTo() {
let s1 = "abcde"
let s2 = "VWXYZ"
let chain = s1.chained(with: s2)
let c = chain(s1, s2)

XCTAssertEqual(chain.count, s1.count + s2.count)
XCTAssertEqual(c.count, s1.count + s2.count)

for (startOffset, endOffset) in product(0...chain.count, 0...chain.count) {
let start = index(atOffset: startOffset, in: chain)
let end = index(atOffset: endOffset, in: chain)
for (startOffset, endOffset) in product(0...c.count, 0...c.count) {
let start = index(atOffset: startOffset, in: c)
let end = index(atOffset: endOffset, in: c)
let distance = endOffset - startOffset
XCTAssertEqual(chain.distance(from: start, to: end), distance)
XCTAssertEqual(c.distance(from: start, to: end), distance)
}
}

func testChainLazy() {
XCTAssertLazy([1, 2, 3].lazy.chained(with: [4, 5, 6]))
XCTAssertLazy(chain([1, 2, 3].lazy, [4, 5, 6]))
}
}