From d0d9ed57fd6161624d90bd3d6dd0759f6ee06246 Mon Sep 17 00:00:00 2001 From: Luciano Almeida Date: Wed, 31 Mar 2021 09:24:55 -0300 Subject: [PATCH 1/5] [Compacted] Implementation of CompactedCollection and CompactedSequence as convenience for .compactMap { $0 } --- Sources/Algorithms/Compacted.swift | 227 +++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 Sources/Algorithms/Compacted.swift diff --git a/Sources/Algorithms/Compacted.swift b/Sources/Algorithms/Compacted.swift new file mode 100644 index 00000000..b429a205 --- /dev/null +++ b/Sources/Algorithms/Compacted.swift @@ -0,0 +1,227 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// A `Sequence` that iterates over every non-nil element from the original `Sequence`. +public struct CompactedSequence: Sequence + where Base.Element == Element? { + + @usableFromInline + let base: Base + + @inlinable + init(base: Base) { + self.base = base + } + + public struct Iterator: IteratorProtocol { + @usableFromInline + var base: Base.Iterator + + @inlinable + init(base: Base.Iterator) { + self.base = base + } + + @inlinable + public mutating func next() -> Element? { + while let wrapped = base.next() { + guard let some = wrapped else { continue } + return some + } + return nil + } + } + + @inlinable + public func makeIterator() -> Iterator { + return Iterator(base: base.makeIterator()) + } +} + +extension Sequence { + /// Returns a new `Sequence` that iterates over every non-nil element + /// from the original `Sequence`. + /// It produces the same result as `c.compactMap { $0 }`. + /// + /// let c = [1, nil, 2, 3, nil] + /// for num in c.compacted() { + /// print(num) + /// } + /// // 1 + /// // 2 + /// // 3 + /// + /// - Returns: A `Sequence` where the element is the unwrapped original + /// element and iterates over every non-nil element from the original + /// `Sequence`. + /// + /// Complexity: O(1) + @inlinable + public func compacted() -> CompactedSequence + where Element == Unwrapped? { + CompactedSequence(base: self) + } +} + +/// A `Collection` that iterates over every non-nil element from the original `Collection`. +public struct CompactedCollection: Collection + where Base.Element == Element? { + + @usableFromInline + let base: Base + + @inlinable + init(base: Base) { + self.base = base + let idx = base.firstIndex(where: { $0 != nil }) ?? base.endIndex + self.startIndex = Index(base: idx) + } + + public struct Index { + @usableFromInline + let base: Base.Index + + @inlinable + init(base: Base.Index) { + self.base = base + } + } + + public var startIndex: Index + + @inlinable + public var endIndex: Index { Index(base: base.endIndex) } + + @inlinable + public subscript(position: Index) -> Element { + base[position.base]! + } + + @inlinable + public func index(after i: Index) -> Index { + precondition(i < endIndex, "Index out of bounds") + + let baseIdx = base.index(after: i.base) + guard let idx = base[baseIdx...].firstIndex(where: { $0 != nil }) + else { return endIndex } + return Index(base: idx) + } +} + +extension CompactedCollection: BidirectionalCollection + where Base: BidirectionalCollection { + + @inlinable + public func index(before i: Index) -> Index { + precondition(i > startIndex, "Index out of bounds") + + guard let idx = + base[startIndex.base.. Bool { + lhs.base < rhs.base + } +} + +extension CompactedCollection.Index: Hashable + where Base.Index: Hashable {} + +extension Collection { + /// Returns a new `Collection` that iterates over every non-nil element + /// from the original `Collection`. + /// It produces the same result as `c.compactMap { $0 }`. + /// + /// let c = [1, nil, 2, 3, nil] + /// for num in c.compacted() { + /// print(num) + /// } + /// // 1 + /// // 2 + /// // 3 + /// + /// - Returns: A `Collection` where the element is the unwrapped original + /// element and iterates over every non-nil element from the original + /// `Collection`. + /// + /// Complexity: O(*n*) where *n* is the number of elements in the + /// original `Collection`. + @inlinable + public func compacted() -> CompactedCollection + where Element == Unwrapped? { + CompactedCollection(base: self) + } +} + +//===----------------------------------------------------------------------===// +// Protocol Conformances +//===----------------------------------------------------------------------===// + +extension CompactedSequence: LazySequenceProtocol + where Base: LazySequenceProtocol {} + +extension CompactedCollection: RandomAccessCollection + where Base: RandomAccessCollection {} +extension CompactedCollection: LazySequenceProtocol + where Base: LazySequenceProtocol {} +extension CompactedCollection: LazyCollectionProtocol + where Base: LazyCollectionProtocol {} + + +// Hashable and Equatable conformance are based on each non-nil +// element on base collection. +extension CompactedSequence: Equatable + where Base.Element: Equatable { + + @inlinable + public static func ==(lhs: CompactedSequence, + rhs: CompactedSequence) -> Bool { + lhs.elementsEqual(rhs) + } +} + +extension CompactedSequence: Hashable + where Element: Hashable { + @inlinable + public func hash(into hasher: inout Hasher) { + for element in self { + hasher.combine(element) + } + } +} + +extension CompactedCollection: Equatable + where Base.Element: Equatable { + + @inlinable + public static func ==(lhs: CompactedCollection, + rhs: CompactedCollection) -> Bool { + lhs.elementsEqual(rhs) + } +} + +extension CompactedCollection: Hashable + where Element: Hashable { + + @inlinable + public func hash(into hasher: inout Hasher) { + for element in self { + hasher.combine(element) + } + } +} From 05491db3bae4ecca7bb422059938f331d36a7879 Mon Sep 17 00:00:00 2001 From: Luciano Almeida Date: Wed, 31 Mar 2021 09:25:41 -0300 Subject: [PATCH 2/5] [tests] [Compacted] Adding tests for compacted() --- .../SwiftAlgorithmsTests/CompactedTests.swift | 68 +++++++++++++++++++ .../SwiftAlgorithmsTests/TestUtilities.swift | 40 +++++++++++ 2 files changed, 108 insertions(+) create mode 100644 Tests/SwiftAlgorithmsTests/CompactedTests.swift diff --git a/Tests/SwiftAlgorithmsTests/CompactedTests.swift b/Tests/SwiftAlgorithmsTests/CompactedTests.swift new file mode 100644 index 00000000..40bdf89f --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/CompactedTests.swift @@ -0,0 +1,68 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +import Algorithms + +final class CompactedTests: XCTestCase { + + let tests: [[Int?]] = + [nil, nil, nil, 0, 1, 2] + .uniquePermutations(ofCount: 0...) + .map(Array.init) + + func testCompactedCompacted() { + for collection in self.tests { + let seq = AnySequence(collection) + XCTAssertEqualSequences( + seq.compactMap({ $0 }), seq.compacted()) + XCTAssertEqualSequences( + collection.compactMap({ $0 }), collection.compacted()) + } + } + + func testCompactedBidirectionalCollection() { + for array in self.tests { + XCTAssertEqualSequences(array.compactMap({ $0 }).reversed(), + array.compacted().reversed()) + } + } + + func testCollectionTraversals() { + for array in self.tests { + validateIndexTraversals(array) + } + } + + func testCollectionEquatableConformances() { + for array in self.tests { + XCTAssertEqual( + array.eraseToAnyHashableSequence().compacted(), + array.compactMap({ $0 }).eraseToAnyHashableSequence().compacted() + ) + XCTAssertEqual( + array.compacted(), array.compactMap({ $0 }).compacted() + ) + } + } + + func testCollectionHashableConformances() { + for array in self.tests { + let seq = array.eraseToAnyHashableSequence() + XCTAssertEqualHashValue( + seq.compacted(), seq.compactMap({ $0 }).compacted() + ) + XCTAssertEqualHashValue( + array.compacted(), array.compactMap({ $0 }).compacted() + ) + } + } +} diff --git a/Tests/SwiftAlgorithmsTests/TestUtilities.swift b/Tests/SwiftAlgorithmsTests/TestUtilities.swift index 6322a997..d3ebdfce 100644 --- a/Tests/SwiftAlgorithmsTests/TestUtilities.swift +++ b/Tests/SwiftAlgorithmsTests/TestUtilities.swift @@ -46,6 +46,27 @@ struct SplitMix64: RandomNumberGenerator { } } +// An eraser helper to any hashable sequence. +struct AnyHashableSequence + where Base: Sequence, Base: Hashable { + var base: Base +} + +extension AnyHashableSequence: Hashable {} +extension AnyHashableSequence: Sequence { + typealias Iterator = Base.Iterator + + func makeIterator() -> Iterator { + base.makeIterator() + } +} + +extension Sequence where Self: Hashable { + func eraseToAnyHashableSequence() -> AnyHashableSequence { + AnyHashableSequence(base: self) + } +} + // An eraser helper to any mutable collection struct AnyMutableCollection where Base: MutableCollection { var base: Base @@ -163,6 +184,25 @@ func XCTAssertEqualCollections( } } +func hash(_ value: T) -> Int { + var hasher = Hasher() + value.hash(into: &hasher) + return hasher.finalize() +} + +/// Asserts two hashable value produce the same hash value. +func XCTAssertEqualHashValue( + _ expression1: @autoclosure () throws -> T, + _ expression2: @autoclosure () throws -> U, + _ message: @autoclosure () -> String = "", + file: StaticString = #file, line: UInt = #line +) { + XCTAssertEqual( + hash(try expression1()), hash(try expression2()), + message(), file: file, line: line + ) +} + /// Tests that all index traversal methods behave as expected. /// /// Verifies the correctness of the implementations of `startIndex`, `endIndex`, From abe4890e7ac2f3ca32e4c034a10a09ce3e198434 Mon Sep 17 00:00:00 2001 From: Luciano Almeida Date: Wed, 31 Mar 2021 09:26:14 -0300 Subject: [PATCH 3/5] [gardening] Adding Guides to compacted() --- Guides/Compacted.md | 39 ++++++++++++++++++++++++++++++ README.md | 1 + Sources/Algorithms/Compacted.swift | 1 + 3 files changed, 41 insertions(+) create mode 100644 Guides/Compacted.md diff --git a/Guides/Compacted.md b/Guides/Compacted.md new file mode 100644 index 00000000..2e361839 --- /dev/null +++ b/Guides/Compacted.md @@ -0,0 +1,39 @@ +# Compacted + +[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Compacted.swift) | + [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/CompactedTests.swift)] + +Convenience method that flatten the `nil`s out of a sequence or collection. +That behaves exactly one of the most common uses of `compactMap` which is `collection.lazy.compactMap { $0 }` +which is only remove `nil`s without transforming the elements. + +```swift +let array: [Int?] = [10, nil, 30, nil, 2, 3, nil, 5] +let withNoNils = array.compacted() +// Array(withNoNils) == [10, 30, 2, 3, 5] + +``` + +The most convenient part of `compacted()` is that we avoid the usage of a closure. + +## Detailed Design + +The `compacted()` methods has two overloads: + +```swift +extension Sequence { + public func compacted() -> CompactedSequence { ... } +} + +extension Collection { + public func compacted() -> CompactedCollection { ... } +} +``` + +One is a more general `CompactedSequence` for any `Sequence` base. And the other a more specialized `CompactedCollection` +where base is a `Collection` and with conditional conformance to `BidirectionalCollection`, `RandomAccessCollection`, +`LazyCollectionProtocol`, `Equatable` and `Hashable` when base collection conforms to them. + +### Naming + +The naming method name `compacted()` matches the current method `compactMap` that one of the most common usages `compactMap { $0 }` is abstracted by it. diff --git a/README.md b/README.md index 7cb65872..77385523 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Read more about the package, and the intent behind it, in the [announcement on s - [`reductions(_:)`, `reductions(_:_:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Reductions.md): Returns all the intermediate states of reducing the elements of a sequence or collection. - [`split(maxSplits:omittingEmptySubsequences:whereSeparator)`, `split(separator:maxSplits:omittingEmptySubsequences)`](https://github.com/apple/swift-algorithms/blob/main/Guides/LazySplit.md): Lazy versions of the Standard Library's eager operations that split sequences and collections into subsequences separated by the specified separator element. - [`windows(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Windows.md): Breaks a collection into overlapping subsequences where elements are slices from the original collection. +- [`compacted()`](https://github.com/apple/swift-algorithms/blob/main/Guides/Compacted.md): Flatten the `nil`s out of a sequence of collection. ## Adding Swift Algorithms as a Dependency diff --git a/Sources/Algorithms/Compacted.swift b/Sources/Algorithms/Compacted.swift index b429a205..bf9b054d 100644 --- a/Sources/Algorithms/Compacted.swift +++ b/Sources/Algorithms/Compacted.swift @@ -197,6 +197,7 @@ extension CompactedSequence: Equatable extension CompactedSequence: Hashable where Element: Hashable { + @inlinable public func hash(into hasher: inout Hasher) { for element in self { From a42bc53b76c30802345e257831366b7730ded936 Mon Sep 17 00:00:00 2001 From: Luciano Almeida Date: Wed, 31 Mar 2021 14:52:18 -0300 Subject: [PATCH 4/5] [CodeReview] Making the corrections --- Sources/Algorithms/Compacted.swift | 6 ++-- .../SwiftAlgorithmsTests/CompactedTests.swift | 31 +++++++++++++------ .../SwiftAlgorithmsTests/TestUtilities.swift | 2 +- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Sources/Algorithms/Compacted.swift b/Sources/Algorithms/Compacted.swift index bf9b054d..304287d2 100644 --- a/Sources/Algorithms/Compacted.swift +++ b/Sources/Algorithms/Compacted.swift @@ -107,7 +107,7 @@ public struct CompactedCollection: Collection @inlinable public func index(after i: Index) -> Index { - precondition(i < endIndex, "Index out of bounds") + precondition(i != endIndex, "Index out of bounds") let baseIdx = base.index(after: i.base) guard let idx = base[baseIdx...].firstIndex(where: { $0 != nil }) @@ -121,7 +121,7 @@ extension CompactedCollection: BidirectionalCollection @inlinable public func index(before i: Index) -> Index { - precondition(i > startIndex, "Index out of bounds") + precondition(i != startIndex, "Index out of bounds") guard let idx = base[startIndex.base..(_ value: T) -> Int { return hasher.finalize() } -/// Asserts two hashable value produce the same hash value. +/// Asserts that two hashable instances produce the same hash value. func XCTAssertEqualHashValue( _ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> U, From d64dd16a6b888bf823f9bdeb8eee3f80492309ea Mon Sep 17 00:00:00 2001 From: Luciano Almeida Date: Mon, 5 Apr 2021 08:15:42 -0300 Subject: [PATCH 5/5] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77385523..0261f714 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Read more about the package, and the intent behind it, in the [announcement on s - [`reductions(_:)`, `reductions(_:_:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Reductions.md): Returns all the intermediate states of reducing the elements of a sequence or collection. - [`split(maxSplits:omittingEmptySubsequences:whereSeparator)`, `split(separator:maxSplits:omittingEmptySubsequences)`](https://github.com/apple/swift-algorithms/blob/main/Guides/LazySplit.md): Lazy versions of the Standard Library's eager operations that split sequences and collections into subsequences separated by the specified separator element. - [`windows(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Windows.md): Breaks a collection into overlapping subsequences where elements are slices from the original collection. -- [`compacted()`](https://github.com/apple/swift-algorithms/blob/main/Guides/Compacted.md): Flatten the `nil`s out of a sequence of collection. +- [`compacted()`](https://github.com/apple/swift-algorithms/blob/main/Guides/Compacted.md): Flatten the `nil`s out of a sequence or collection. ## Adding Swift Algorithms as a Dependency