Skip to content

Commit

Permalink
Make uniqued() lazy by default (#71)
Browse files Browse the repository at this point in the history
* Make `uniqued()` lazy by default
  • Loading branch information
Tim Vermeulen authored Apr 7, 2021
1 parent 551b4af commit 718220d
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 16 deletions.
21 changes: 14 additions & 7 deletions Guides/Unique.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

Methods to strip repeated elements from a sequence or collection.

The `uniqued()` method returns an array, dropping duplicate elements
The `uniqued()` method returns a sequence, dropping duplicate elements
from a sequence. The `uniqued(on:)` method does the same, using
the result of the given closure to determine the "uniqueness" of each
element.
Expand All @@ -14,29 +14,36 @@ element.
let numbers = [1, 2, 3, 3, 2, 3, 3, 2, 2, 2, 1]

let unique = numbers.uniqued()
// unique == [1, 2, 3]
// Array(unique) == [1, 2, 3]
```

## Detailed Design

Both methods are available for sequences, with the simplest limited to
when the element type conforms to `Hashable`. Both methods preserve
the relative order of the elements.
the relative order of the elements. `uniqued(on:)` has a matching lazy
version that is added to `LazySequenceProtocol`.

```swift
extension Sequence where Element: Hashable {
func uniqued() -> [Element]
func uniqued() -> Uniqued<Self, Element>
}

extension Sequence {
func uniqued<T>(on: (Element) throws -> T) rethrows -> [Element]
where T: Hashable
func uniqued<Subject>(on projection: (Element) throws -> Subject) rethrows -> [Element]
where Subject: Hashable
}

extension LazySequenceProtocol {
func uniqued<Subject>(on projection: @escaping (Element) -> Subject) -> Uniqued<Self, Subject>
where Subject: Hashable
}
```

### Complexity

The `uniqued` methods are O(_n_) in both time and space complexity.
The eager `uniqued(on:)` method is O(_n_) in both time and space complexity.
The lazy versions are O(_1_).

### Comparison with other languages

Expand Down
89 changes: 82 additions & 7 deletions Sources/Algorithms/Unique.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,82 @@
//
//===----------------------------------------------------------------------===//

/// A sequence wrapper that leaves out duplicate elements of a base sequence.
public struct Uniqued<Base: Sequence, Subject: Hashable> {
/// The base collection.
@usableFromInline
internal let base: Base

/// The projection function.
@usableFromInline
internal let projection: (Base.Element) -> Subject

@usableFromInline
internal init(base: Base, projection: @escaping (Base.Element) -> Subject) {
self.base = base
self.projection = projection
}
}

extension Uniqued: Sequence {
/// The iterator for a `Uniqued` sequence.
public struct Iterator: IteratorProtocol {
@usableFromInline
internal var base: Base.Iterator

@usableFromInline
internal let projection: (Base.Element) -> Subject

@usableFromInline
internal var seen: Set<Subject> = []

@usableFromInline
internal init(
base: Base.Iterator,
projection: @escaping (Base.Element) -> Subject
) {
self.base = base
self.projection = projection
}

@inlinable
public mutating func next() -> Base.Element? {
while let element = base.next() {
if seen.insert(projection(element)).inserted {
return element
}
}
return nil
}
}

@inlinable
public func makeIterator() -> Iterator {
Iterator(base: base.makeIterator(), projection: projection)
}
}

extension Uniqued: LazySequenceProtocol where Base: LazySequenceProtocol {}

//===----------------------------------------------------------------------===//
// uniqued()
//===----------------------------------------------------------------------===//

extension Sequence where Element: Hashable {
/// Returns an array with only the unique elements of this sequence, in the
/// Returns a sequence with only the unique elements of this sequence, in the
/// order of the first occurrence of each unique element.
///
/// let animals = ["dog", "pig", "cat", "ox", "dog", "cat"]
/// let uniqued = animals.uniqued()
/// print(uniqued)
/// print(Array(uniqued))
/// // Prints '["dog", "pig", "cat", "ox"]'
///
/// - Returns: An array with only the unique elements of this sequence.
/// - Returns: A sequence with only the unique elements of this sequence.
/// .
/// - Complexity: O(*n*), where *n* is the length of the sequence.
/// - Complexity: O(1).
@inlinable
public func uniqued() -> [Element] {
uniqued(on: { $0 })
public func uniqued() -> Uniqued<Self, Element> {
Uniqued(base: self, projection: { $0 })
}
}

Expand All @@ -40,7 +97,7 @@ extension Sequence {
/// first characters:
///
/// let animals = ["dog", "pig", "cat", "ox", "cow", "owl"]
/// let uniqued = animals.uniqued(on: {$0.first})
/// let uniqued = animals.uniqued(on: { $0.first })
/// print(uniqued)
/// // Prints '["dog", "pig", "cat", "ox"]'
///
Expand All @@ -67,3 +124,21 @@ extension Sequence {
return result
}
}

//===----------------------------------------------------------------------===//
// lazy.uniqued()
//===----------------------------------------------------------------------===//

extension LazySequenceProtocol {
/// Returns a lazy sequence with the unique elements of this sequence (as
/// determined by the given projection), in the order of the first occurrence
/// of each unique element.
///
/// - Complexity: O(1).
@inlinable
public func uniqued<Subject: Hashable>(
on projection: @escaping (Element) -> Subject
) -> Uniqued<Self, Subject> {
Uniqued(base: self, projection: projection)
}
}
23 changes: 21 additions & 2 deletions Tests/SwiftAlgorithmsTests/UniqueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ final class UniqueTests: XCTestCase {
let a = repeatElement(1...10, count: 15).joined().shuffled()
let b = a.uniqued()
XCTAssertEqual(b.sorted(), Set(a).sorted())
XCTAssertEqual(10, b.count)
XCTAssertEqual(10, Array(b).count)

let c: [Int] = []
XCTAssertEqual(c.uniqued(), [])
XCTAssertEqualSequences(c.uniqued(), [])

let d = Array(repeating: 1, count: 10)
XCTAssertEqualSequences(d.uniqued(), [1])
}

func testUniqueOn() {
Expand All @@ -30,5 +33,21 @@ final class UniqueTests: XCTestCase {

let c: [Int] = []
XCTAssertEqual(c.uniqued(on: { $0.bitWidth }), [])

let d = Array(repeating: "Andromeda", count: 10)
XCTAssertEqualSequences(d.uniqued(on: { $0.first }), ["Andromeda"])
}

func testLazyUniqueOn() {
let a = ["Albemarle", "Abeforth", "Astrology", "Brandywine", "Beatrice", "Axiom"]
let b = a.lazy.uniqued(on: { $0.first })
XCTAssertEqualSequences(b, ["Albemarle", "Brandywine"])
XCTAssertLazySequence(b)

let c: [Int] = []
XCTAssertEqualSequences(c.lazy.uniqued(on: { $0.bitWidth }), [])

let d = Array(repeating: "Andromeda", count: 10)
XCTAssertEqualSequences(d.lazy.uniqued(on: { $0.first }), ["Andromeda"])
}
}

0 comments on commit 718220d

Please sign in to comment.