Skip to content

Commit

Permalink
Add joined(by:) (#138)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tim Vermeulen authored Jun 1, 2021
1 parent 0e2941e commit 4b779b8
Show file tree
Hide file tree
Showing 7 changed files with 1,692 additions and 20 deletions.
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

0 comments on commit 4b779b8

Please sign in to comment.