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 joined(by:) #138

Merged
merged 4 commits into from
Jun 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
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
102 changes: 102 additions & 0 deletions Guides/Joined.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Joined

[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Joined.swift) |
[Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/JoinedTests.swift)]

Concatenate a sequence of sequences, inserting a separator between each element.

The separator can be either a single element or a sequence of elements, and it
can optionally depend on the sequences right before and after it by returning it
from a closure:

```swift
for number in [[1], [2, 3], [4, 5, 6]].joined(by: 100) {
print(number)
}
// 1, 100, 2, 3, 100, 4, 5, 6

for number in [[10], [20, 30], [40, 50, 60]].joined(by: { [$0.count, $1.count] }) {
print(number)
}
// 10, 1, 2, 20, 30, 2, 3, 40, 50, 60
```

## Detailed Design

The versions that take a closure are executed eagerly and are defined on
`Sequence`:

```swift
extension Sequence where Element: Sequence {
public func joined(
by separator: (Element, Element) throws -> Element.Element
) rethrows -> [Element.Element]

public func joined<Separator>(
by separator: (Element, Element) throws -> Separator
) rethrows -> [Element.Element]
where Separator: Sequence, Separator.Element == Element.Element
}
```

The versions that do not take a closure are defined on both `Sequence` and
`Collection` because the resulting collections need to precompute their start
index to ensure O(1) access:

```swift
extension Sequence where Element: Sequence {
public func joined(by separator: Element.Element)
-> JoinedBySequence<Self, CollectionOfOne<Element.Element>>

public func joined<Separator>(
by separator: Separator
) -> JoinedBySequence<Self, Separator>
where Separator: Collection, Separator.Element == Element.Element
}

extension Collection where Element: Sequence {
public func joined(by separator: Element.Element)
-> JoinedByCollection<Self, CollectionOfOne<Element.Element>>

public func joined<Separator>(
by separator: Separator
) -> JoinedByCollection<Self, Separator>
where Separator: Collection, Separator.Element == Element.Element
}
```

Note that the sequence separator of the closure-less version defined on
`Sequence` is required to be a `Collection`, because a plain `Sequence` cannot in
general be iterated over multiple times.

The closure-based versions also have lazy variants that are defined on both
`LazySequenceProtocol` and `LazyCollectionProtocol` for the same reason as
explained above:

```swift
extension LazySequenceProtocol where Element: Sequence {
public func joined(
by separator: @escaping (Element, Element) -> Element.Element
) -> JoinedByClosureSequence<Self, CollectionOfOne<Element.Element>>

public func joined<Separator>(
by separator: @escaping (Element, Element) -> Separator
) -> JoinedByClosureSequence<Self, Separator>
}

extension LazyCollectionProtocol where Element: Collection {
public func joined(
by separator: @escaping (Element, Element) -> Element.Element
) -> JoinedByClosureCollection<Self, CollectionOfOne<Element.Element>>

public func joined<Separator>(
by separator: @escaping (Element, Element) -> Separator
) -> JoinedByClosureCollection<Self, Separator>
}
```

`JoinedBySequence`, `JoinedByClosureSequence`, `JoinedByCollection`, and
`JoinedByClosureCollection` conform to `LazySequenceProtocol` when the base
sequence conforms. `JoinedByCollection` and `JoinedByClosureCollection` also
conform to `LazyCollectionProtocol` and `BidirectionalCollection` when the base
collection conforms.
200 changes: 200 additions & 0 deletions Sources/Algorithms/EitherSequence.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Algorithms open source project
//
// Copyright (c) 2021 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

//===----------------------------------------------------------------------===//
// Either
//===----------------------------------------------------------------------===//

/// A general-purpose sum type.
@usableFromInline
internal enum Either<Left, Right> {
case left(Left)
case right(Right)
}

extension Either: Equatable where Left: Equatable, Right: Equatable {
@usableFromInline
internal static func == (lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
case let (.left(lhs), .left(rhs)):
return lhs == rhs
case let (.right(lhs), .right(rhs)):
return lhs == rhs
case (.left, .right), (.right, .left):
return false
}
}
}

extension Either: Comparable where Left: Comparable, Right: Comparable {
@usableFromInline
internal static func < (lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
case let (.left(lhs), .left(rhs)):
return lhs < rhs
case let (.right(lhs), .right(rhs)):
return lhs < rhs
case (.left, .right):
return true
case (.right, .left):
return false
}
}
}

//===----------------------------------------------------------------------===//
// EitherSequence
//===----------------------------------------------------------------------===//

/// A sequence that has one of the two specified types.
@usableFromInline
internal enum EitherSequence<Left: Sequence, Right: Sequence>
where Left.Element == Right.Element
{
case left(Left)
case right(Right)
}

extension EitherSequence: Sequence {
@usableFromInline
internal struct Iterator: IteratorProtocol {
@usableFromInline
internal var left: Left.Iterator?

@usableFromInline
internal var right: Right.Iterator?

@inlinable
internal mutating func next() -> Left.Element? {
left?.next() ?? right?.next()
}
}

@usableFromInline
internal func makeIterator() -> Iterator {
switch self {
case .left(let left):
return Iterator(left: left.makeIterator(), right: nil)
case .right(let right):
return Iterator(left: nil, right: right.makeIterator())
}
}
}

extension EitherSequence: Collection
where Left: Collection, Right: Collection, Left.Element == Right.Element
{
@usableFromInline
internal typealias Index = Either<Left.Index, Right.Index>

@inlinable
internal var startIndex: Index {
switch self {
case .left(let s):
return .left(s.startIndex)
case .right(let s):
return .right(s.startIndex)
}
}

@inlinable
internal var endIndex: Index {
switch self {
case .left(let s):
return .left(s.endIndex)
case .right(let s):
return .right(s.endIndex)
}
}

@inlinable
internal subscript(position: Index) -> Element {
switch (self, position) {
case let (.left(s), .left(i)):
return s[i]
case let (.right(s), .right(i)):
return s[i]
default:
fatalError()
}
}

@inlinable
internal func index(after i: Index) -> Index {
switch (self,i) {
case let (.left(s), .left(i)):
return .left(s.index(after: i))
case let (.right(s), .right(i)):
return .right(s.index(after: i))
default:
fatalError()
}
}

@inlinable
internal func index(
_ i: Index,
offsetBy distance: Int,
limitedBy limit: Index
) -> Index? {
switch (self, i, limit) {
case let (.left(s), .left(i), .left(limit)):
return s.index(i, offsetBy: distance, limitedBy: limit).map { .left($0) }
case let (.right(s), .right(i), .right(limit)):
return s.index(i, offsetBy: distance, limitedBy: limit).map { .right($0) }
default:
fatalError()
}
}

@inlinable
internal func index(_ i: Index, offsetBy distance: Int) -> Index {
switch (self, i) {
case let (.left(s), .left(i)):
return .left(s.index(i, offsetBy: distance))
case let (.right(s), .right(i)):
return .right(s.index(i, offsetBy: distance))
default:
fatalError()
}
}

@inlinable
internal func distance(from start: Index, to end: Index) -> Int {
switch (self, start, end) {
case let (.left(s), .left(i), .left(j)):
return s.distance(from: i, to: j)
case let (.right(s), .right(i), .right(j)):
return s.distance(from: i, to: j)
default:
fatalError()
}
}
}

extension EitherSequence: BidirectionalCollection
where Left: BidirectionalCollection, Right: BidirectionalCollection
{
@inlinable
internal func index(before i: Index) -> Index {
switch (self, i) {
case let (.left(s), .left(i)):
return .left(s.index(before: i))
case let (.right(s), .right(i)):
return .right(s.index(before: i))
default:
fatalError()
}
}
}

extension EitherSequence: RandomAccessCollection
where Left: RandomAccessCollection, Right: RandomAccessCollection {}
Loading