From 4bba172eec8342a8bb49aedf06481a7013a248b8 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Fri, 9 Oct 2020 18:18:13 +0100 Subject: [PATCH 01/45] Add scan function --- Guides/Scan.md | 53 +++++++++++++++++ Sources/Algorithms/Scan.swift | 67 ++++++++++++++++++++++ Tests/SwiftAlgorithmsTests/ScanTests.swift | 20 +++++++ 3 files changed, 140 insertions(+) create mode 100644 Guides/Scan.md create mode 100644 Sources/Algorithms/Scan.swift create mode 100644 Tests/SwiftAlgorithmsTests/ScanTests.swift diff --git a/Guides/Scan.md b/Guides/Scan.md new file mode 100644 index 00000000..97098ee8 --- /dev/null +++ b/Guides/Scan.md @@ -0,0 +1,53 @@ +# Scan + +[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Scan.swift) | + [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/ScanTests.swift)] + +Produces a sequence of values. + +This has the behaviour of reduce, but instead of returning the final result +value, it returns the a sequence of the results returned from each element. + +```swift +let accumulation = (1...5).scan(0, +) +print(Array(accumulation)) +// prints [1, 3, 6, 10, 15] + +let runningMinimum = [3, 4, 2, 3, 1].scan(.max, min) +print(Array(runningMinimum)) +// prints [3, 3, 2, 2, 1] +``` + +## Detailed Design + +One new method is added to sequences: + +```swift +extension Sequence { + func scan( + _ initial: Result, + _ transform: @escaping (Result, Element) -> Result + ) -> Scan +} +``` + +### Complexity + +Calling these methods is O(_1_). + +### Naming + + + +### Comparison with other langauges + +**C++:** As of C++17, the `` library includes an `inclusive_scan` function. + +**Haskell:** Haskell includes a `scan` function for its `Traversable` type, +which is akin to Swift's `Sequence`. + +**Python:** Python’s `itertools` includes an `accumulate` method. In version +3.3, a function paramenter was added. Version 3.8 added the optional initial +parameter. + +**Rust:** Rust provides a `scan` function. diff --git a/Sources/Algorithms/Scan.swift b/Sources/Algorithms/Scan.swift new file mode 100644 index 00000000..55a36a70 --- /dev/null +++ b/Sources/Algorithms/Scan.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2020 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 of applying a transform to the element of a sequence and the +/// previously transformed result. +public struct Scan { + let base: Base + let initial: Result + let transform: (Result, Base.Element) -> Result +} + +extension Scan: Sequence { + public struct Iterator: IteratorProtocol { + var iterator: Base.Iterator + var current: Result + let transform: (Result, Base.Element) -> Result + + public mutating func next() -> Result? { + guard let element = iterator.next() else { return nil } + current = transform(current, element) + return current + } + } + + public func makeIterator() -> Iterator { + Iterator(iterator: base.makeIterator(), + current: initial, + transform: transform) + } +} + +extension Sequence { + + /// Returns a sequence containing the results of combining the elements of + /// the sequence using the given transform. + /// + /// This can be seen as applying the reduce function to each element and + /// providing these results as a sequence. + /// + /// ``` + /// let values = [1, 2, 3, 4] + /// let sequence = values.scan(0, +) + /// print(Array(sequence)) + /// + /// // prints [1, 3, 6, 10] + /// ``` + /// + /// - Parameters: + /// - initial: The value to use as the initial accumulating value. + /// - transform: A closure that combines an accumulating value and + /// an element of the sequence. + /// - Returns: A sequence of transformed elements. + public func scan( + _ initial: Result, + _ transform: @escaping (Result, Element) -> Result + ) -> Scan { + Scan(base: self, initial: initial, transform: transform) + } +} diff --git a/Tests/SwiftAlgorithmsTests/ScanTests.swift b/Tests/SwiftAlgorithmsTests/ScanTests.swift new file mode 100644 index 00000000..42db08c2 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/ScanTests.swift @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2020 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 ScanTests: XCTestCase { + func testScan() { + XCTAssertEqualSequences((1...5).scan(0, +), [1, 3, 6, 10, 15]) + XCTAssertEqualSequences([3, 4, 2, 3, 1].scan(.max, min), [3, 3, 2, 2, 1]) + } +} From 548b1e7aed0a3b5e229a3b29ff37b1ea775cd3cc Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Tue, 24 Nov 2020 12:12:47 +0000 Subject: [PATCH 02/45] Add initial conditional conformances to Collection and BidirectionalCollection --- Sources/Algorithms/Scan.swift | 36 ++++++++++++++++++++++ Tests/SwiftAlgorithmsTests/ScanTests.swift | 29 +++++++++++++++-- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/Sources/Algorithms/Scan.swift b/Sources/Algorithms/Scan.swift index 55a36a70..a557f47b 100644 --- a/Sources/Algorithms/Scan.swift +++ b/Sources/Algorithms/Scan.swift @@ -37,6 +37,42 @@ extension Scan: Sequence { } } +extension Scan: Collection where Base: Collection { + public var startIndex: Base.Index { + base.startIndex + } + + public var endIndex: Base.Index { + base.endIndex + } + + public subscript(position: Base.Index) -> Result { + base[...position].reduce(initial, transform) + } + + public func index(after i: Base.Index) -> Base.Index { + base.index(after: i) + } + + public func index(_ i: Base.Index, offsetBy distance: Int) -> Base.Index { + base.index(i, offsetBy: distance) + } + + public func index(_ i: Base.Index, offsetBy distance: Int, limitedBy limit: Base.Index) -> Base.Index? { + base.index(i, offsetBy: distance, limitedBy: limit) + } + + public func distance(from start: Base.Index, to end: Base.Index) -> Int { + base.distance(from: start, to: end) + } +} + +extension Scan: BidirectionalCollection where Base: BidirectionalCollection { + public func index(before i: Base.Index) -> Base.Index { + base.index(before: i) + } +} + extension Sequence { /// Returns a sequence containing the results of combining the elements of diff --git a/Tests/SwiftAlgorithmsTests/ScanTests.swift b/Tests/SwiftAlgorithmsTests/ScanTests.swift index 42db08c2..800331a8 100644 --- a/Tests/SwiftAlgorithmsTests/ScanTests.swift +++ b/Tests/SwiftAlgorithmsTests/ScanTests.swift @@ -13,8 +13,31 @@ import XCTest import Algorithms final class ScanTests: XCTestCase { - func testScan() { - XCTAssertEqualSequences((1...5).scan(0, +), [1, 3, 6, 10, 15]) - XCTAssertEqualSequences([3, 4, 2, 3, 1].scan(.max, min), [3, 3, 2, 2, 1]) + func testSequence() { + let scan = (1...).prefix(5).scan(0, +) + XCTAssertEqualSequences(scan, [1, 3, 6, 10, 15]) + } + + func testSequenceEmpty() { + let scan = (1...).prefix(0).scan(0, +) + XCTAssertEqualSequences(scan, []) + } + + func testCollection() { + let scan = [3, 4, 2, 3, 1].scan(.max, min) + XCTAssertEqualSequences(scan, [3, 3, 2, 2, 1]) + validateIndexTraversals(scan) + } + + func testCollectionEmpty() { + let scan = EmptyCollection().scan(.max, min) + XCTAssertEqualSequences(scan, []) + validateIndexTraversals(scan) + } + + func testBidirectionalCollection() { + let reversed = [1,2,3,4,5].scan(0, +).reversed() + XCTAssertEqualSequences(reversed, [15, 10, 6, 3, 1]) + validateIndexTraversals(reversed) } } From e9edde07a0b73d1a46ceae73ab0a1ac7867feba7 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Tue, 24 Nov 2020 12:15:33 +0000 Subject: [PATCH 03/45] Rename scan to reductions --- Guides/{Scan.md => Reductions.md} | 14 +++++------ .../{Scan.swift => Reductions.swift} | 16 ++++++------- ...{ScanTests.swift => ReductionsTests.swift} | 24 +++++++++---------- 3 files changed, 27 insertions(+), 27 deletions(-) rename Guides/{Scan.md => Reductions.md} (81%) rename Sources/Algorithms/{Scan.swift => Reductions.swift} (86%) rename Tests/SwiftAlgorithmsTests/{ScanTests.swift => ReductionsTests.swift} (54%) diff --git a/Guides/Scan.md b/Guides/Reductions.md similarity index 81% rename from Guides/Scan.md rename to Guides/Reductions.md index 97098ee8..3f8cfedb 100644 --- a/Guides/Scan.md +++ b/Guides/Reductions.md @@ -1,7 +1,7 @@ -# Scan +# Reductions -[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Scan.swift) | - [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/ScanTests.swift)] +[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Reductions.swift) | + [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/ReductionsTests.swift)] Produces a sequence of values. @@ -9,11 +9,11 @@ This has the behaviour of reduce, but instead of returning the final result value, it returns the a sequence of the results returned from each element. ```swift -let accumulation = (1...5).scan(0, +) +let accumulation = (1...5).reductions(0, +) print(Array(accumulation)) // prints [1, 3, 6, 10, 15] -let runningMinimum = [3, 4, 2, 3, 1].scan(.max, min) +let runningMinimum = [3, 4, 2, 3, 1].reductions(.max, min) print(Array(runningMinimum)) // prints [3, 3, 2, 2, 1] ``` @@ -24,10 +24,10 @@ One new method is added to sequences: ```swift extension Sequence { - func scan( + func reductions( _ initial: Result, _ transform: @escaping (Result, Element) -> Result - ) -> Scan + ) -> Reductions } ``` diff --git a/Sources/Algorithms/Scan.swift b/Sources/Algorithms/Reductions.swift similarity index 86% rename from Sources/Algorithms/Scan.swift rename to Sources/Algorithms/Reductions.swift index a557f47b..bead9650 100644 --- a/Sources/Algorithms/Scan.swift +++ b/Sources/Algorithms/Reductions.swift @@ -11,13 +11,13 @@ /// A sequence of applying a transform to the element of a sequence and the /// previously transformed result. -public struct Scan { +public struct Reductions { let base: Base let initial: Result let transform: (Result, Base.Element) -> Result } -extension Scan: Sequence { +extension Reductions: Sequence { public struct Iterator: IteratorProtocol { var iterator: Base.Iterator var current: Result @@ -37,7 +37,7 @@ extension Scan: Sequence { } } -extension Scan: Collection where Base: Collection { +extension Reductions: Collection where Base: Collection { public var startIndex: Base.Index { base.startIndex } @@ -67,7 +67,7 @@ extension Scan: Collection where Base: Collection { } } -extension Scan: BidirectionalCollection where Base: BidirectionalCollection { +extension Reductions: BidirectionalCollection where Base: BidirectionalCollection { public func index(before i: Base.Index) -> Base.Index { base.index(before: i) } @@ -83,7 +83,7 @@ extension Sequence { /// /// ``` /// let values = [1, 2, 3, 4] - /// let sequence = values.scan(0, +) + /// let sequence = values.reductions(0, +) /// print(Array(sequence)) /// /// // prints [1, 3, 6, 10] @@ -94,10 +94,10 @@ extension Sequence { /// - transform: A closure that combines an accumulating value and /// an element of the sequence. /// - Returns: A sequence of transformed elements. - public func scan( + public func reductions( _ initial: Result, _ transform: @escaping (Result, Element) -> Result - ) -> Scan { - Scan(base: self, initial: initial, transform: transform) + ) -> Reductions { + Reductions(base: self, initial: initial, transform: transform) } } diff --git a/Tests/SwiftAlgorithmsTests/ScanTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift similarity index 54% rename from Tests/SwiftAlgorithmsTests/ScanTests.swift rename to Tests/SwiftAlgorithmsTests/ReductionsTests.swift index 800331a8..19bfe770 100644 --- a/Tests/SwiftAlgorithmsTests/ScanTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -12,31 +12,31 @@ import XCTest import Algorithms -final class ScanTests: XCTestCase { +final class ReductionsTests: XCTestCase { func testSequence() { - let scan = (1...).prefix(5).scan(0, +) - XCTAssertEqualSequences(scan, [1, 3, 6, 10, 15]) + let reductions = (1...).prefix(5).reductions(0, +) + XCTAssertEqualSequences(reductions, [1, 3, 6, 10, 15]) } func testSequenceEmpty() { - let scan = (1...).prefix(0).scan(0, +) - XCTAssertEqualSequences(scan, []) + let reductions = (1...).prefix(0).reductions(0, +) + XCTAssertEqualSequences(reductions, []) } func testCollection() { - let scan = [3, 4, 2, 3, 1].scan(.max, min) - XCTAssertEqualSequences(scan, [3, 3, 2, 2, 1]) - validateIndexTraversals(scan) + let reductions = [3, 4, 2, 3, 1].reductions(.max, min) + XCTAssertEqualSequences(reductions, [3, 3, 2, 2, 1]) + validateIndexTraversals(reductions) } func testCollectionEmpty() { - let scan = EmptyCollection().scan(.max, min) - XCTAssertEqualSequences(scan, []) - validateIndexTraversals(scan) + let reductions = EmptyCollection().reductions(.max, min) + XCTAssertEqualSequences(reductions, []) + validateIndexTraversals(reductions) } func testBidirectionalCollection() { - let reversed = [1,2,3,4,5].scan(0, +).reversed() + let reversed = [1,2,3,4,5].reductions(0, +).reversed() XCTAssertEqualSequences(reversed, [15, 10, 6, 3, 1]) validateIndexTraversals(reversed) } From b41af13bc924e80614bbddae090b674aa60100f0 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Tue, 24 Nov 2020 13:09:04 +0000 Subject: [PATCH 04/45] Add information to the readme about the reductions name --- Guides/Reductions.md | 47 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/Guides/Reductions.md b/Guides/Reductions.md index 3f8cfedb..71552e56 100644 --- a/Guides/Reductions.md +++ b/Guides/Reductions.md @@ -9,8 +9,8 @@ This has the behaviour of reduce, but instead of returning the final result value, it returns the a sequence of the results returned from each element. ```swift -let accumulation = (1...5).reductions(0, +) -print(Array(accumulation)) +let runningTotal = (1...5).reductions(0, +) +print(Array(runningTotal)) // prints [1, 3, 6, 10, 15] let runningMinimum = [3, 4, 2, 3, 1].reductions(.max, min) @@ -37,17 +37,52 @@ Calling these methods is O(_1_). ### Naming +While the name `scan` is the term of art for this function, it has been +discussed that `reductions` aligns better with the existing `reduce` function +and will aid newcomers that might not know the existing `scan` term. +Below are two quotes from the Swift forum [discussion about SE-0045][SE-0045] +which proposed adding `scan` to the standard library and one from +[issue #25][Issue 25] on the swift-algorithms GitHub project. These provide +the reasoning to use the name `reductions`. + +[Brent Royal-Gordon][Brent_Royal-Gordon]: +> I really like the `reduce`/`reductions` pairing instead of `reduce`/`scan`; +it does a really good job of explaining the relationship between the two +functions. + +[David Rönnqvist][David Rönnqvist]: +> As other have already pointed out, I also feel that `scan` is the least +intuitive name among these and that the `reduce`/`reductions` pairing would do +a good job at explaining the relation between the two. + +[Kyle Macomber][Kyle Macomber]: +> As someone unfamiliar with the prior art, `reductions` strikes me as very +approachable—I feel like I can extrapolate the expected behavior purely from my +familiarity with `reduce`. + +[SE-0045]: https://forums.swift.org/t/review-se-0045-add-scan-prefix-while-drop-while-and-iterate-to-the-stdlib/2382 +[Issue 25]: https://github.com/apple/swift-algorithms/issues/25 +[Brent_Royal-Gordon]: https://forums.swift.org/t/review-se-0045-add-scan-prefix-while-drop-while-and-iterate-to-the-stdlib/2382/6 +[David Rönnqvist]: https://forums.swift.org/t/review-se-0045-add-scan-prefix-while-drop-while-and-iterate-to-the-stdlib/2382/8 +[Kyle Macomber]: https://github.com/apple/swift-algorithms/issues/25#issuecomment-709317894 ### Comparison with other langauges -**C++:** As of C++17, the `` library includes an `inclusive_scan` function. +**C++:** As of C++17, the `` library includes an `inclusive_scan` +function. + +**[Clojure][Clojure]:** Clojure 1.2 added a `reductions` function. -**Haskell:** Haskell includes a `scan` function for its `Traversable` type, -which is akin to Swift's `Sequence`. +**[Haskell][Haskell]:** Haskell includes a `scan` function for its +`Traversable` type, which is akin to Swift's `Sequence`. **Python:** Python’s `itertools` includes an `accumulate` method. In version 3.3, a function paramenter was added. Version 3.8 added the optional initial parameter. -**Rust:** Rust provides a `scan` function. +**[Rust][Rust]:** Rust provides a `scan` function. + +[Clojure]: http://clojure.github.io/clojure/clojure.core-api.html#clojure.core/reductions +[Haskell]: http://hackage.haskell.org/package/base-4.8.2.0/docs/Prelude.html#v:scanl +[Rust]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.scan From 5116e2500231e25bcfed06bd09cbd8f89d2d9059 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Wed, 25 Nov 2020 08:46:05 +0000 Subject: [PATCH 05/45] Add eager API --- Sources/Algorithms/Reductions.swift | 15 +++++++++++++++ Tests/SwiftAlgorithmsTests/ReductionsTests.swift | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index bead9650..344b986a 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -75,6 +75,21 @@ extension Reductions: BidirectionalCollection where Base: BidirectionalCollectio extension Sequence { + public func reductions( + _ initial: Result, + _ transform: (Result, Element) throws -> Result + ) rethrows -> [Result] { + + var result = initial + return try map { element in + result = try transform(result, element) + return result + } + } +} + +extension LazySequenceProtocol { + /// Returns a sequence containing the results of combining the elements of /// the sequence using the given transform. /// diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index 19bfe770..aae38616 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -23,6 +23,18 @@ final class ReductionsTests: XCTestCase { XCTAssertEqualSequences(reductions, []) } + func testEager() { + let reductions: [Int] = [3, 4, 2, 3, 1].reductions(.max, min) + XCTAssertEqualSequences(reductions, [3, 3, 2, 2, 1]) + validateIndexTraversals(reductions) + } + + func testEagerThrows() { + struct E: Error {} + XCTAssertNoThrow(try [].reductions(0, { _, _ in throw E() })) + XCTAssertThrowsError(try [1].reductions(0, { _, _ in throw E() })) + } + func testCollection() { let reductions = [3, 4, 2, 3, 1].reductions(.max, min) XCTAssertEqualSequences(reductions, [3, 3, 2, 2, 1]) From e9d35f60758fa288e8c56e6dc76ba5cc5f9cb236 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 26 Nov 2020 10:42:55 +0000 Subject: [PATCH 06/45] Remove Reductions' conformance to BidirectionalCollection --- Sources/Algorithms/Reductions.swift | 6 ------ Tests/SwiftAlgorithmsTests/ReductionsTests.swift | 6 ------ 2 files changed, 12 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 344b986a..467db8e3 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -67,12 +67,6 @@ extension Reductions: Collection where Base: Collection { } } -extension Reductions: BidirectionalCollection where Base: BidirectionalCollection { - public func index(before i: Base.Index) -> Base.Index { - base.index(before: i) - } -} - extension Sequence { public func reductions( diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index aae38616..c4f20bb5 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -46,10 +46,4 @@ final class ReductionsTests: XCTestCase { XCTAssertEqualSequences(reductions, []) validateIndexTraversals(reductions) } - - func testBidirectionalCollection() { - let reversed = [1,2,3,4,5].reductions(0, +).reversed() - XCTAssertEqualSequences(reversed, [15, 10, 6, 3, 1]) - validateIndexTraversals(reversed) - } } From caf0b7175c31e097d191c3ed584e2a8f0aadf0fd Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 26 Nov 2020 10:43:26 +0000 Subject: [PATCH 07/45] Implement Reductions subscript such that it occurs in constant time --- Sources/Algorithms/Reductions.swift | 41 ++++++++++++++++++----------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 467db8e3..5dc1e256 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -38,32 +38,43 @@ extension Reductions: Sequence { } extension Reductions: Collection where Base: Collection { - public var startIndex: Base.Index { - base.startIndex - } + public struct Index: Comparable { + let index: Base.Index + let result: Result + + public static func < (lhs: Index, rhs: Index) -> Bool { + lhs.index < rhs.index + } - public var endIndex: Base.Index { - base.endIndex + public static func == (lhs: Index, rhs: Index) -> Bool { + lhs.index == rhs.index + } } - public subscript(position: Base.Index) -> Result { - base[...position].reduce(initial, transform) + public var startIndex: Index { + let start = base.startIndex + let result = transform(initial, base[start]) + return Index(index: start, result: result) } - public func index(after i: Base.Index) -> Base.Index { - base.index(after: i) + public var endIndex: Index { + let end = base.endIndex + let result = base.reduce(initial, transform) + return Index(index: end, result: result) } - public func index(_ i: Base.Index, offsetBy distance: Int) -> Base.Index { - base.index(i, offsetBy: distance) + public subscript(position: Index) -> Result { + position.result } - public func index(_ i: Base.Index, offsetBy distance: Int, limitedBy limit: Base.Index) -> Base.Index? { - base.index(i, offsetBy: distance, limitedBy: limit) + public func index(after i: Index) -> Index { + let index = base.index(after: i.index) + let result = transform(i.result, base[i.index]) + return Index(index: index, result: result) } - public func distance(from start: Base.Index, to end: Base.Index) -> Int { - base.distance(from: start, to: end) + public func distance(from start: Index, to end: Index) -> Int { + base.distance(from: start.index, to: end.index) } } From 531b6f91878eb80b0a4ded1e4cd51831e88e5d80 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 26 Nov 2020 12:06:29 +0000 Subject: [PATCH 08/45] Add a variant which includes the given initial result --- Sources/Algorithms/Reductions.swift | 18 ++++++++++++++++++ .../SwiftAlgorithmsTests/ReductionsTests.swift | 7 +++++++ 2 files changed, 25 insertions(+) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 5dc1e256..8fe6a483 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -80,6 +80,24 @@ extension Reductions: Collection where Base: Collection { extension Sequence { + public func reductions( + including initial: Result, + _ transform: (Result, Element) throws -> Result + ) rethrows -> [Result] { + + var output = [Result]() + output.reserveCapacity(underestimatedCount + 1) + output.append(initial) + + var result = initial + for element in self { + result = try transform(result, element) + output.append(result) + } + + return output + } + public func reductions( _ initial: Result, _ transform: (Result, Element) throws -> Result diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index c4f20bb5..74b0e271 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -27,12 +27,19 @@ final class ReductionsTests: XCTestCase { let reductions: [Int] = [3, 4, 2, 3, 1].reductions(.max, min) XCTAssertEqualSequences(reductions, [3, 3, 2, 2, 1]) validateIndexTraversals(reductions) + + let including: [Int] = [6, 3, 2, 4, 1].reductions(including: 10, +) + XCTAssertEqualSequences(including, [10, 16, 19, 21, 25, 26]) + validateIndexTraversals(including) } func testEagerThrows() { struct E: Error {} XCTAssertNoThrow(try [].reductions(0, { _, _ in throw E() })) XCTAssertThrowsError(try [1].reductions(0, { _, _ in throw E() })) + + XCTAssertNoThrow(try [].reductions(including: 0, { _, _ in throw E() })) + XCTAssertThrowsError(try [1].reductions(including: 0, { _, _ in throw E() })) } func testCollection() { From c0c237a944ac635787d3c2177ee1b62816e914d4 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 26 Nov 2020 12:07:40 +0000 Subject: [PATCH 09/45] Add a variant which takes no initial result value --- Sources/Algorithms/Reductions.swift | 10 ++++++++++ Tests/SwiftAlgorithmsTests/ReductionsTests.swift | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 8fe6a483..9806c50e 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -78,6 +78,16 @@ extension Reductions: Collection where Base: Collection { } } +extension Collection { + public func reductions( + _ transform: (Element, Element) throws -> Element + ) rethrows -> [Element] { + + guard let first = first else { return [] } + return try dropFirst().reductions(including: first, transform) + } +} + extension Sequence { public func reductions( diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index 74b0e271..2cc320ea 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -31,6 +31,10 @@ final class ReductionsTests: XCTestCase { let including: [Int] = [6, 3, 2, 4, 1].reductions(including: 10, +) XCTAssertEqualSequences(including, [10, 16, 19, 21, 25, 26]) validateIndexTraversals(including) + + let noInitial: [Int] = [3, 4, 2, 3, 1].reductions(+) + XCTAssertEqualSequences(noInitial, [3, 7, 9, 12, 13]) + validateIndexTraversals(noInitial) } func testEagerThrows() { @@ -40,6 +44,10 @@ final class ReductionsTests: XCTestCase { XCTAssertNoThrow(try [].reductions(including: 0, { _, _ in throw E() })) XCTAssertThrowsError(try [1].reductions(including: 0, { _, _ in throw E() })) + + XCTAssertNoThrow(try [].reductions({ _, _ in throw E() })) + XCTAssertNoThrow(try [1].reductions({ _, _ in throw E() })) + XCTAssertThrowsError(try [1, 1].reductions({ _, _ in throw E() })) } func testCollection() { From 7bb96ead8958a5bb602f7d3a9d308bfdb53c2103 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 26 Nov 2020 12:19:05 +0000 Subject: [PATCH 10/45] Add excluding label to show the initial result is not included --- Sources/Algorithms/Reductions.swift | 2 +- .../SwiftAlgorithmsTests/ReductionsTests.swift | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 9806c50e..0d41279b 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -109,7 +109,7 @@ extension Sequence { } public func reductions( - _ initial: Result, + excluding initial: Result, _ transform: (Result, Element) throws -> Result ) rethrows -> [Result] { diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index 2cc320ea..af185ea6 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -14,19 +14,19 @@ import Algorithms final class ReductionsTests: XCTestCase { func testSequence() { - let reductions = (1...).prefix(5).reductions(0, +) + let reductions = (1...).prefix(5).reductions(excluding: 0, +) XCTAssertEqualSequences(reductions, [1, 3, 6, 10, 15]) } func testSequenceEmpty() { - let reductions = (1...).prefix(0).reductions(0, +) + let reductions = (1...).prefix(0).reductions(excluding: 0, +) XCTAssertEqualSequences(reductions, []) } func testEager() { - let reductions: [Int] = [3, 4, 2, 3, 1].reductions(.max, min) - XCTAssertEqualSequences(reductions, [3, 3, 2, 2, 1]) - validateIndexTraversals(reductions) + let excluding: [Int] = [3, 4, 2, 3, 1].reductions(excluding: .max, min) + XCTAssertEqualSequences(excluding, [3, 3, 2, 2, 1]) + validateIndexTraversals(excluding) let including: [Int] = [6, 3, 2, 4, 1].reductions(including: 10, +) XCTAssertEqualSequences(including, [10, 16, 19, 21, 25, 26]) @@ -39,8 +39,8 @@ final class ReductionsTests: XCTestCase { func testEagerThrows() { struct E: Error {} - XCTAssertNoThrow(try [].reductions(0, { _, _ in throw E() })) - XCTAssertThrowsError(try [1].reductions(0, { _, _ in throw E() })) + XCTAssertNoThrow(try [].reductions(excluding: 0, { _, _ in throw E() })) + XCTAssertThrowsError(try [1].reductions(excluding: 0, { _, _ in throw E() })) XCTAssertNoThrow(try [].reductions(including: 0, { _, _ in throw E() })) XCTAssertThrowsError(try [1].reductions(including: 0, { _, _ in throw E() })) @@ -51,13 +51,13 @@ final class ReductionsTests: XCTestCase { } func testCollection() { - let reductions = [3, 4, 2, 3, 1].reductions(.max, min) + let reductions = [3, 4, 2, 3, 1].reductions(excluding: .max, min) XCTAssertEqualSequences(reductions, [3, 3, 2, 2, 1]) validateIndexTraversals(reductions) } func testCollectionEmpty() { - let reductions = EmptyCollection().reductions(.max, min) + let reductions = EmptyCollection().reductions(excluding: .max, min) XCTAssertEqualSequences(reductions, []) validateIndexTraversals(reductions) } From 5b4d984a08afb5904f3cb603e5d291aa87a5a411 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 26 Nov 2020 17:05:36 +0000 Subject: [PATCH 11/45] Test lazy implementations --- .../ReductionsTests.swift | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index af185ea6..8c311dba 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -13,16 +13,28 @@ import XCTest import Algorithms final class ReductionsTests: XCTestCase { - func testSequence() { - let reductions = (1...).prefix(5).reductions(excluding: 0, +) + func testLazySequence() { + let reductions = (1...).prefix(5).lazy.reductions(0, +) XCTAssertEqualSequences(reductions, [1, 3, 6, 10, 15]) } - func testSequenceEmpty() { - let reductions = (1...).prefix(0).reductions(excluding: 0, +) + func testLazySequenceEmpty() { + let reductions = (1...).prefix(0).lazy.reductions(0, +) XCTAssertEqualSequences(reductions, []) } + func testLazyCollection() { + let reductions = [3, 4, 2, 3, 1].lazy.reductions(.max, min) + XCTAssertEqualSequences(reductions, [3, 3, 2, 2, 1]) +// validateIndexTraversals(reductions) + } + + func testLazyCollectionEmpty() { + let reductions = EmptyCollection().lazy.reductions(.max, min) + XCTAssertEqualSequences(reductions, []) +// validateIndexTraversals(reductions) + } + func testEager() { let excluding: [Int] = [3, 4, 2, 3, 1].reductions(excluding: .max, min) XCTAssertEqualSequences(excluding, [3, 3, 2, 2, 1]) @@ -49,16 +61,4 @@ final class ReductionsTests: XCTestCase { XCTAssertNoThrow(try [1].reductions({ _, _ in throw E() })) XCTAssertThrowsError(try [1, 1].reductions({ _, _ in throw E() })) } - - func testCollection() { - let reductions = [3, 4, 2, 3, 1].reductions(excluding: .max, min) - XCTAssertEqualSequences(reductions, [3, 3, 2, 2, 1]) - validateIndexTraversals(reductions) - } - - func testCollectionEmpty() { - let reductions = EmptyCollection().reductions(excluding: .max, min) - XCTAssertEqualSequences(reductions, []) - validateIndexTraversals(reductions) - } } From e0ee01d6534e1d56f6f609892671940c92e5df34 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 26 Nov 2020 17:12:25 +0000 Subject: [PATCH 12/45] Remove reductions(including:_:) --- Sources/Algorithms/Reductions.swift | 16 ++-------------- .../SwiftAlgorithmsTests/ReductionsTests.swift | 17 +++++------------ 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 0d41279b..5d567e3c 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -84,14 +84,14 @@ extension Collection { ) rethrows -> [Element] { guard let first = first else { return [] } - return try dropFirst().reductions(including: first, transform) + return try dropFirst().reductions(first, transform) } } extension Sequence { public func reductions( - including initial: Result, + _ initial: Result, _ transform: (Result, Element) throws -> Result ) rethrows -> [Result] { @@ -107,18 +107,6 @@ extension Sequence { return output } - - public func reductions( - excluding initial: Result, - _ transform: (Result, Element) throws -> Result - ) rethrows -> [Result] { - - var result = initial - return try map { element in - result = try transform(result, element) - return result - } - } } extension LazySequenceProtocol { diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index 8c311dba..b98257cb 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -36,13 +36,9 @@ final class ReductionsTests: XCTestCase { } func testEager() { - let excluding: [Int] = [3, 4, 2, 3, 1].reductions(excluding: .max, min) - XCTAssertEqualSequences(excluding, [3, 3, 2, 2, 1]) - validateIndexTraversals(excluding) - - let including: [Int] = [6, 3, 2, 4, 1].reductions(including: 10, +) - XCTAssertEqualSequences(including, [10, 16, 19, 21, 25, 26]) - validateIndexTraversals(including) + let initial: [Int] = [6, 3, 2, 4, 1].reductions(10, +) + XCTAssertEqualSequences(initial, [10, 16, 19, 21, 25, 26]) + validateIndexTraversals(initial) let noInitial: [Int] = [3, 4, 2, 3, 1].reductions(+) XCTAssertEqualSequences(noInitial, [3, 7, 9, 12, 13]) @@ -51,11 +47,8 @@ final class ReductionsTests: XCTestCase { func testEagerThrows() { struct E: Error {} - XCTAssertNoThrow(try [].reductions(excluding: 0, { _, _ in throw E() })) - XCTAssertThrowsError(try [1].reductions(excluding: 0, { _, _ in throw E() })) - - XCTAssertNoThrow(try [].reductions(including: 0, { _, _ in throw E() })) - XCTAssertThrowsError(try [1].reductions(including: 0, { _, _ in throw E() })) + XCTAssertNoThrow(try [].reductions(0, { _, _ in throw E() })) + XCTAssertThrowsError(try [1].reductions(0, { _, _ in throw E() })) XCTAssertNoThrow(try [].reductions({ _, _ in throw E() })) XCTAssertNoThrow(try [1].reductions({ _, _ in throw E() })) From 1ce31ed773ac0505f7842b9498eda5dc55d6599a Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 26 Nov 2020 20:24:38 +0000 Subject: [PATCH 13/45] Add conformance to LazySequenceProtocol and LazyCollectionProtocol when the base sequence conforms --- Sources/Algorithms/Reductions.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 5d567e3c..c29d21f0 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -78,6 +78,12 @@ extension Reductions: Collection where Base: Collection { } } +extension Reductions: LazyCollectionProtocol +where Base: LazyCollectionProtocol {} + +extension Reductions: LazySequenceProtocol + where Base: LazySequenceProtocol {} + extension Collection { public func reductions( _ transform: (Element, Element) throws -> Element From fe912e5b67a90c3cf12f2912ff8592a98c4d45bc Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Fri, 27 Nov 2020 07:56:44 +0000 Subject: [PATCH 14/45] Fix implementation of lazy reductions --- Guides/Reductions.md | 19 ++++++- Sources/Algorithms/Reductions.swift | 18 +++---- .../ReductionsTests.swift | 50 ++++++++++--------- 3 files changed, 53 insertions(+), 34 deletions(-) diff --git a/Guides/Reductions.md b/Guides/Reductions.md index 71552e56..dfd9405f 100644 --- a/Guides/Reductions.md +++ b/Guides/Reductions.md @@ -11,7 +11,7 @@ value, it returns the a sequence of the results returned from each element. ```swift let runningTotal = (1...5).reductions(0, +) print(Array(runningTotal)) -// prints [1, 3, 6, 10, 15] +// prints [0, 1, 3, 6, 10, 15] let runningMinimum = [3, 4, 2, 3, 1].reductions(.max, min) print(Array(runningMinimum)) @@ -23,12 +23,27 @@ print(Array(runningMinimum)) One new method is added to sequences: ```swift -extension Sequence { +extension LazySequenceProtocol { func reductions( _ initial: Result, _ transform: @escaping (Result, Element) -> Result ) -> Reductions } + +extension Sequence { + public func reductions( + _ initial: Result, + _ transform: (Result, Element) throws -> Result + ) rethrows -> [Result] +} +``` + +```swift +extension Collection { + public func reductions( + _ transform: (Element, Element) throws -> Element + ) rethrows -> [Element] +} ``` ### Complexity diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index c29d21f0..a09a63ee 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -20,13 +20,13 @@ public struct Reductions { extension Reductions: Sequence { public struct Iterator: IteratorProtocol { var iterator: Base.Iterator - var current: Result + var current: Result? let transform: (Result, Base.Element) -> Result public mutating func next() -> Result? { - guard let element = iterator.next() else { return nil } - current = transform(current, element) - return current + guard let result = current else { return nil } + current = iterator.next().map { transform(result, $0) } + return result } } @@ -121,20 +121,20 @@ extension LazySequenceProtocol { /// the sequence using the given transform. /// /// This can be seen as applying the reduce function to each element and - /// providing these results as a sequence. + /// providing the initial value followed by these results as a sequence. /// /// ``` /// let values = [1, 2, 3, 4] /// let sequence = values.reductions(0, +) /// print(Array(sequence)) /// - /// // prints [1, 3, 6, 10] + /// // prints [0, 1, 3, 6, 10] /// ``` /// /// - Parameters: - /// - initial: The value to use as the initial accumulating value. - /// - transform: A closure that combines an accumulating value and - /// an element of the sequence. + /// - initial: The value to use as the initial value. + /// - transform: A closure that combines the previously reduced result and + /// the next element in the receiving sequence. /// - Returns: A sequence of transformed elements. public func reductions( _ initial: Result, diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index b98257cb..ce4ffa8d 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -13,45 +13,49 @@ import XCTest import Algorithms final class ReductionsTests: XCTestCase { + func testLazySequence() { let reductions = (1...).prefix(5).lazy.reductions(0, +) - XCTAssertEqualSequences(reductions, [1, 3, 6, 10, 15]) - } + XCTAssertEqualSequences(reductions, [0, 1, 3, 6, 10, 15]) - func testLazySequenceEmpty() { - let reductions = (1...).prefix(0).lazy.reductions(0, +) - XCTAssertEqualSequences(reductions, []) + let empty = (1...).prefix(0).lazy.reductions(0, +) + XCTAssertEqualSequences(empty, [0]) } func testLazyCollection() { - let reductions = [3, 4, 2, 3, 1].lazy.reductions(.max, min) - XCTAssertEqualSequences(reductions, [3, 3, 2, 2, 1]) + let reductions = [1, 2, 3, 4, 5].lazy.reductions(0, +) + XCTAssertEqualSequences(reductions, [0, 1, 3, 6, 10, 15]) // validateIndexTraversals(reductions) - } - func testLazyCollectionEmpty() { - let reductions = EmptyCollection().lazy.reductions(.max, min) - XCTAssertEqualSequences(reductions, []) + let empty = EmptyCollection().lazy.reductions(0, +) + XCTAssertEqualSequences(empty, [0]) // validateIndexTraversals(reductions) } - func testEager() { - let initial: [Int] = [6, 3, 2, 4, 1].reductions(10, +) - XCTAssertEqualSequences(initial, [10, 16, 19, 21, 25, 26]) - validateIndexTraversals(initial) + func testEagerInitial() { + let reductions: [Int] = [1, 2, 3, 4, 5].reductions(0, +) + XCTAssertEqualSequences(reductions, [0, 1, 3, 6, 10, 15]) - let noInitial: [Int] = [3, 4, 2, 3, 1].reductions(+) - XCTAssertEqualSequences(noInitial, [3, 7, 9, 12, 13]) - validateIndexTraversals(noInitial) + let empty: [Int] = EmptyCollection().reductions(0, +) + XCTAssertEqualSequences(empty, [0]) + } + + func testEagerNoInitial() { + let reductions: [Int] = [1, 2, 3, 4, 5].reductions(+) + XCTAssertEqualSequences(reductions, [1, 3, 6, 10, 15]) + + let empty: [Int] = EmptyCollection().reductions(+) + XCTAssertEqualSequences(empty, []) } func testEagerThrows() { struct E: Error {} - XCTAssertNoThrow(try [].reductions(0, { _, _ in throw E() })) - XCTAssertThrowsError(try [1].reductions(0, { _, _ in throw E() })) - XCTAssertNoThrow(try [].reductions({ _, _ in throw E() })) - XCTAssertNoThrow(try [1].reductions({ _, _ in throw E() })) - XCTAssertThrowsError(try [1, 1].reductions({ _, _ in throw E() })) + XCTAssertNoThrow(try [].reductions(0) { _, _ in throw E() }) + XCTAssertThrowsError(try [1].reductions(0) { _, _ in throw E() }) + + XCTAssertNoThrow(try [].reductions { _, _ in throw E() }) + XCTAssertNoThrow(try [1].reductions { _, _ in throw E() }) + XCTAssertThrowsError(try [1, 1].reductions { _, _ in throw E() }) } } From c3e918d61bd5e410db60199c192fdc13637a7d77 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Fri, 27 Nov 2020 08:05:22 +0000 Subject: [PATCH 15/45] Add test cases for sequences with one element --- Tests/SwiftAlgorithmsTests/ReductionsTests.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index ce4ffa8d..424614a0 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -18,6 +18,9 @@ final class ReductionsTests: XCTestCase { let reductions = (1...).prefix(5).lazy.reductions(0, +) XCTAssertEqualSequences(reductions, [0, 1, 3, 6, 10, 15]) + let one = (1...).prefix(1).lazy.reductions(0, +) + XCTAssertEqualSequences(one, [0, 1]) + let empty = (1...).prefix(0).lazy.reductions(0, +) XCTAssertEqualSequences(empty, [0]) } @@ -27,8 +30,13 @@ final class ReductionsTests: XCTestCase { XCTAssertEqualSequences(reductions, [0, 1, 3, 6, 10, 15]) // validateIndexTraversals(reductions) + let one = [1].lazy.reductions(0, +) + XCTAssertEqualSequences(one, [0, 1]) +// validateIndexTraversals(one) + let empty = EmptyCollection().lazy.reductions(0, +) XCTAssertEqualSequences(empty, [0]) +// validateIndexTraversals(empty) // validateIndexTraversals(reductions) } @@ -36,6 +44,9 @@ final class ReductionsTests: XCTestCase { let reductions: [Int] = [1, 2, 3, 4, 5].reductions(0, +) XCTAssertEqualSequences(reductions, [0, 1, 3, 6, 10, 15]) + let one: [Int] = CollectionOfOne(1).reductions(0, +) + XCTAssertEqualSequences(one, [0, 1]) + let empty: [Int] = EmptyCollection().reductions(0, +) XCTAssertEqualSequences(empty, [0]) } @@ -44,6 +55,9 @@ final class ReductionsTests: XCTestCase { let reductions: [Int] = [1, 2, 3, 4, 5].reductions(+) XCTAssertEqualSequences(reductions, [1, 3, 6, 10, 15]) + let one: [Int] = CollectionOfOne(1).reductions(+) + XCTAssertEqualSequences(one, [1]) + let empty: [Int] = EmptyCollection().reductions(+) XCTAssertEqualSequences(empty, []) } From c69357931ab19f5245fc843f69c530f025cf7aab Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Fri, 27 Nov 2020 08:10:14 +0000 Subject: [PATCH 16/45] Tidy up tests --- .../ReductionsTests.swift | 72 ++++++++++--------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index 424614a0..d2e3ba2e 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -14,52 +14,60 @@ import Algorithms final class ReductionsTests: XCTestCase { - func testLazySequence() { - let reductions = (1...).prefix(5).lazy.reductions(0, +) - XCTAssertEqualSequences(reductions, [0, 1, 3, 6, 10, 15]) - - let one = (1...).prefix(1).lazy.reductions(0, +) - XCTAssertEqualSequences(one, [0, 1]) - - let empty = (1...).prefix(0).lazy.reductions(0, +) - XCTAssertEqualSequences(empty, [0]) + func testLazySequenceInitial() { + XCTAssertEqualSequences( + (1...).prefix(5).lazy.reductions(0, +), + [0, 1, 3, 6, 10, 15]) + + XCTAssertEqualSequences( + (1...).prefix(1).lazy.reductions(0, +), + [0, 1]) + + XCTAssertEqualSequences( + (1...).prefix(0).lazy.reductions(0, +), + [0]) } - func testLazyCollection() { - let reductions = [1, 2, 3, 4, 5].lazy.reductions(0, +) - XCTAssertEqualSequences(reductions, [0, 1, 3, 6, 10, 15]) -// validateIndexTraversals(reductions) + func testLazyCollectionInitial() { + XCTAssertEqualSequences( + [1, 2, 3, 4, 5].lazy.reductions(0, +), + [0, 1, 3, 6, 10, 15]) - let one = [1].lazy.reductions(0, +) - XCTAssertEqualSequences(one, [0, 1]) -// validateIndexTraversals(one) + XCTAssertEqualSequences( + [1].lazy.reductions(0, +), + [0, 1]) - let empty = EmptyCollection().lazy.reductions(0, +) - XCTAssertEqualSequences(empty, [0]) -// validateIndexTraversals(empty) -// validateIndexTraversals(reductions) + XCTAssertEqualSequences( + EmptyCollection().lazy.reductions(0, +), + [0]) } func testEagerInitial() { - let reductions: [Int] = [1, 2, 3, 4, 5].reductions(0, +) - XCTAssertEqualSequences(reductions, [0, 1, 3, 6, 10, 15]) + XCTAssertEqual( + [1, 2, 3, 4, 5].reductions(0, +), + [0, 1, 3, 6, 10, 15]) - let one: [Int] = CollectionOfOne(1).reductions(0, +) - XCTAssertEqualSequences(one, [0, 1]) + XCTAssertEqual( + CollectionOfOne(1).reductions(0, +), + [0, 1]) - let empty: [Int] = EmptyCollection().reductions(0, +) - XCTAssertEqualSequences(empty, [0]) + XCTAssertEqualSequences( + EmptyCollection().reductions(0, +), + [0]) } func testEagerNoInitial() { - let reductions: [Int] = [1, 2, 3, 4, 5].reductions(+) - XCTAssertEqualSequences(reductions, [1, 3, 6, 10, 15]) + XCTAssertEqualSequences( + [1, 2, 3, 4, 5].reductions(+), + [1, 3, 6, 10, 15]) - let one: [Int] = CollectionOfOne(1).reductions(+) - XCTAssertEqualSequences(one, [1]) + XCTAssertEqualSequences( + CollectionOfOne(1).reductions(+), + [1]) - let empty: [Int] = EmptyCollection().reductions(+) - XCTAssertEqualSequences(empty, []) + XCTAssertEqualSequences( + EmptyCollection().reductions(+), + []) } func testEagerThrows() { From 55e87eb3d83c823c286e1d66bd172cf087a536ac Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Fri, 27 Nov 2020 08:21:08 +0000 Subject: [PATCH 17/45] Improve ergonomics of no initial value eager reductions call and provide it as an extension on Sequence --- Sources/Algorithms/Reductions.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index a09a63ee..7b3c2de6 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -84,17 +84,15 @@ where Base: LazyCollectionProtocol {} extension Reductions: LazySequenceProtocol where Base: LazySequenceProtocol {} -extension Collection { +extension Sequence { public func reductions( _ transform: (Element, Element) throws -> Element ) rethrows -> [Element] { - guard let first = first else { return [] } - return try dropFirst().reductions(first, transform) + var iterator = makeIterator() + guard let initial = iterator.next() else { return [] } + return try IteratorSequence(iterator).reductions(initial, transform) } -} - -extension Sequence { public func reductions( _ initial: Result, From 4654ae758e3216516499b144e075f6d9ad544ac3 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Fri, 27 Nov 2020 15:26:07 +0000 Subject: [PATCH 18/45] Add lazy InclusiveReductions sequence --- Sources/Algorithms/Reductions.swift | 62 ++++++++++++++++--- .../ReductionsTests.swift | 38 ++++++------ 2 files changed, 74 insertions(+), 26 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 7b3c2de6..94242eac 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -9,6 +9,8 @@ // //===----------------------------------------------------------------------===// +// MARK: - Exclusive Reductions + /// A sequence of applying a transform to the element of a sequence and the /// previously transformed result. public struct Reductions { @@ -85,14 +87,6 @@ extension Reductions: LazySequenceProtocol where Base: LazySequenceProtocol {} extension Sequence { - public func reductions( - _ transform: (Element, Element) throws -> Element - ) rethrows -> [Element] { - - var iterator = makeIterator() - guard let initial = iterator.next() else { return [] } - return try IteratorSequence(iterator).reductions(initial, transform) - } public func reductions( _ initial: Result, @@ -141,3 +135,55 @@ extension LazySequenceProtocol { Reductions(base: self, initial: initial, transform: transform) } } + +// MARK: - Inclusive Reductions + +extension LazySequenceProtocol { + + public func reductions( + _ transform: @escaping (Element, Element) -> Element + ) -> InclusiveReductions { + InclusiveReductions(base: self, transform: transform) + } +} + +extension Sequence { + public func reductions( + _ transform: (Element, Element) throws -> Element + ) rethrows -> [Element] { + var iterator = makeIterator() + guard let initial = iterator.next() else { return [] } + return try IteratorSequence(iterator).reductions(initial, transform) + } +} + +public struct InclusiveReductions { + let base: Base + let transform: (Base.Element, Base.Element) -> Base.Element +} + +extension InclusiveReductions: Sequence { + public struct Iterator: IteratorProtocol { + var iterator: Base.Iterator + var element: Base.Element? + let transform: (Base.Element, Base.Element) -> Base.Element + + public mutating func next() -> Base.Element? { + guard let previous = element else { + element = iterator.next() + return element + } + guard let next = iterator.next() else { return nil } + element = transform(previous, next) + return element + } + } + + public func makeIterator() -> Iterator { + Iterator(iterator: base.makeIterator(), + transform: transform) + } +} + +extension InclusiveReductions: LazySequenceProtocol + where Base: LazySequenceProtocol {} diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index d2e3ba2e..699c3f61 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -14,6 +14,10 @@ import Algorithms final class ReductionsTests: XCTestCase { + struct TestError: Error {} + + // MARK: - Exclusive Reductions + func testLazySequenceInitial() { XCTAssertEqualSequences( (1...).prefix(5).lazy.reductions(0, +), @@ -56,28 +60,26 @@ final class ReductionsTests: XCTestCase { [0]) } - func testEagerNoInitial() { - XCTAssertEqualSequences( - [1, 2, 3, 4, 5].reductions(+), - [1, 3, 6, 10, 15]) + func testEagerThrows() { + XCTAssertNoThrow(try [].reductions(0) { _, _ in throw TestError() }) + XCTAssertThrowsError(try [1].reductions(0) { _, _ in throw TestError() }) + } - XCTAssertEqualSequences( - CollectionOfOne(1).reductions(+), - [1]) + // MARK: - Inclusive Reductions - XCTAssertEqualSequences( - EmptyCollection().reductions(+), - []) + func testInclusiveLazySequence() { + XCTAssertEqualSequences((1...).prefix(4).lazy.reductions(+), [1, 3, 6, 10]) + XCTAssertEqualSequences((1...).prefix(1).lazy.reductions(+), [1]) + XCTAssertEqualSequences((1...).prefix(0).lazy.reductions(+), []) } - func testEagerThrows() { - struct E: Error {} - - XCTAssertNoThrow(try [].reductions(0) { _, _ in throw E() }) - XCTAssertThrowsError(try [1].reductions(0) { _, _ in throw E() }) + func testInclusiveEager() { + XCTAssertEqual([1, 2, 3, 4].reductions(+), [1, 3, 6, 10]) + XCTAssertEqual([1].reductions(+), [1]) + XCTAssertEqual(EmptyCollection().reductions(+), []) - XCTAssertNoThrow(try [].reductions { _, _ in throw E() }) - XCTAssertNoThrow(try [1].reductions { _, _ in throw E() }) - XCTAssertThrowsError(try [1, 1].reductions { _, _ in throw E() }) + XCTAssertNoThrow(try [].reductions { _, _ in throw TestError() }) + XCTAssertNoThrow(try [1].reductions { _, _ in throw TestError() }) + XCTAssertThrowsError(try [1, 1].reductions { _, _ in throw TestError() }) } } From 154fa4066ed5fa14453480e2280b55bdbfeac16e Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Fri, 27 Nov 2020 15:30:40 +0000 Subject: [PATCH 19/45] Rename Reductions to ExclusiveReductions --- Sources/Algorithms/Reductions.swift | 148 ++++++------------ .../ReductionsTests.swift | 48 +----- 2 files changed, 60 insertions(+), 136 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 94242eac..e3d39662 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -11,81 +11,35 @@ // MARK: - Exclusive Reductions -/// A sequence of applying a transform to the element of a sequence and the -/// previously transformed result. -public struct Reductions { - let base: Base - let initial: Result - let transform: (Result, Base.Element) -> Result -} - -extension Reductions: Sequence { - public struct Iterator: IteratorProtocol { - var iterator: Base.Iterator - var current: Result? - let transform: (Result, Base.Element) -> Result - - public mutating func next() -> Result? { - guard let result = current else { return nil } - current = iterator.next().map { transform(result, $0) } - return result - } - } - - public func makeIterator() -> Iterator { - Iterator(iterator: base.makeIterator(), - current: initial, - transform: transform) - } -} - -extension Reductions: Collection where Base: Collection { - public struct Index: Comparable { - let index: Base.Index - let result: Result - - public static func < (lhs: Index, rhs: Index) -> Bool { - lhs.index < rhs.index - } - - public static func == (lhs: Index, rhs: Index) -> Bool { - lhs.index == rhs.index - } - } - - public var startIndex: Index { - let start = base.startIndex - let result = transform(initial, base[start]) - return Index(index: start, result: result) - } - - public var endIndex: Index { - let end = base.endIndex - let result = base.reduce(initial, transform) - return Index(index: end, result: result) - } - - public subscript(position: Index) -> Result { - position.result - } - - public func index(after i: Index) -> Index { - let index = base.index(after: i.index) - let result = transform(i.result, base[i.index]) - return Index(index: index, result: result) - } +extension LazySequenceProtocol { - public func distance(from start: Index, to end: Index) -> Int { - base.distance(from: start.index, to: end.index) + /// Returns a sequence containing the results of combining the elements of + /// the sequence using the given transform. + /// + /// This can be seen as applying the reduce function to each element and + /// providing the initial value followed by these results as a sequence. + /// + /// ``` + /// let values = [1, 2, 3, 4] + /// let sequence = values.reductions(0, +) + /// print(Array(sequence)) + /// + /// // prints [0, 1, 3, 6, 10] + /// ``` + /// + /// - Parameters: + /// - initial: The value to use as the initial value. + /// - transform: A closure that combines the previously reduced result and + /// the next element in the receiving sequence. + /// - Returns: A sequence of transformed elements. + public func reductions( + _ initial: Result, + _ transform: @escaping (Result, Element) -> Result + ) -> ExclusiveReductions { + ExclusiveReductions(base: self, initial: initial, transform: transform) } } -extension Reductions: LazyCollectionProtocol -where Base: LazyCollectionProtocol {} - -extension Reductions: LazySequenceProtocol - where Base: LazySequenceProtocol {} - extension Sequence { public func reductions( @@ -107,35 +61,37 @@ extension Sequence { } } -extension LazySequenceProtocol { +/// A sequence of applying a transform to the element of a sequence and the +/// previously transformed result. +public struct ExclusiveReductions { + let base: Base + let initial: Result + let transform: (Result, Base.Element) -> Result +} - /// Returns a sequence containing the results of combining the elements of - /// the sequence using the given transform. - /// - /// This can be seen as applying the reduce function to each element and - /// providing the initial value followed by these results as a sequence. - /// - /// ``` - /// let values = [1, 2, 3, 4] - /// let sequence = values.reductions(0, +) - /// print(Array(sequence)) - /// - /// // prints [0, 1, 3, 6, 10] - /// ``` - /// - /// - Parameters: - /// - initial: The value to use as the initial value. - /// - transform: A closure that combines the previously reduced result and - /// the next element in the receiving sequence. - /// - Returns: A sequence of transformed elements. - public func reductions( - _ initial: Result, - _ transform: @escaping (Result, Element) -> Result - ) -> Reductions { - Reductions(base: self, initial: initial, transform: transform) +extension ExclusiveReductions: Sequence { + public struct Iterator: IteratorProtocol { + var iterator: Base.Iterator + var current: Result? + let transform: (Result, Base.Element) -> Result + + public mutating func next() -> Result? { + guard let result = current else { return nil } + current = iterator.next().map { transform(result, $0) } + return result + } + } + + public func makeIterator() -> Iterator { + Iterator(iterator: base.makeIterator(), + current: initial, + transform: transform) } } +extension ExclusiveReductions: LazySequenceProtocol + where Base: LazySequenceProtocol {} + // MARK: - Inclusive Reductions extension LazySequenceProtocol { diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index 699c3f61..fe258619 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -18,49 +18,17 @@ final class ReductionsTests: XCTestCase { // MARK: - Exclusive Reductions - func testLazySequenceInitial() { - XCTAssertEqualSequences( - (1...).prefix(5).lazy.reductions(0, +), - [0, 1, 3, 6, 10, 15]) - - XCTAssertEqualSequences( - (1...).prefix(1).lazy.reductions(0, +), - [0, 1]) - - XCTAssertEqualSequences( - (1...).prefix(0).lazy.reductions(0, +), - [0]) + func testExclusiveLazySequence() { + XCTAssertEqualSequences((1...).prefix(4).lazy.reductions(0, +), [0, 1, 3, 6, 10]) + XCTAssertEqualSequences((1...).prefix(1).lazy.reductions(0, +), [0, 1]) + XCTAssertEqualSequences((1...).prefix(0).lazy.reductions(0, +), [0]) } - func testLazyCollectionInitial() { - XCTAssertEqualSequences( - [1, 2, 3, 4, 5].lazy.reductions(0, +), - [0, 1, 3, 6, 10, 15]) - - XCTAssertEqualSequences( - [1].lazy.reductions(0, +), - [0, 1]) - - XCTAssertEqualSequences( - EmptyCollection().lazy.reductions(0, +), - [0]) - } - - func testEagerInitial() { - XCTAssertEqual( - [1, 2, 3, 4, 5].reductions(0, +), - [0, 1, 3, 6, 10, 15]) - - XCTAssertEqual( - CollectionOfOne(1).reductions(0, +), - [0, 1]) - - XCTAssertEqualSequences( - EmptyCollection().reductions(0, +), - [0]) - } + func testExclusiveEager() { + XCTAssertEqual([1, 2, 3, 4].reductions(0, +), [0, 1, 3, 6, 10]) + XCTAssertEqual([1].reductions(0, +), [0, 1]) + XCTAssertEqual(EmptyCollection().reductions(0, +), [0]) - func testEagerThrows() { XCTAssertNoThrow(try [].reductions(0) { _, _ in throw TestError() }) XCTAssertThrowsError(try [1].reductions(0) { _, _ in throw TestError() }) } From 959e963a167f8300fc99b5d3a9fc5363b0a6fff9 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Fri, 27 Nov 2020 16:12:32 +0000 Subject: [PATCH 20/45] Add Collection implementation for InclusiveReductions --- Sources/Algorithms/Reductions.swift | 45 +++++++++++++++++++ .../ReductionsTests.swift | 12 ++++- .../SwiftAlgorithmsTests/TestUtilities.swift | 15 +++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index e3d39662..95335463 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -141,5 +141,50 @@ extension InclusiveReductions: Sequence { } } +extension InclusiveReductions: Collection where Base: Collection { + public struct Index: Comparable { + let index: Base.Index + let previous: Element? + + public static func < (lhs: Index, rhs: Index) -> Bool { + lhs.index < rhs.index + } + + public static func == (lhs: Index, rhs: Index) -> Bool { + lhs.index == rhs.index + } + } + + public var startIndex: Index { + Index(index: base.startIndex, previous: nil) + } + + public var endIndex: Index { + var iterator = makeIterator() + guard let initial = iterator.next() else { return startIndex } + let previous = IteratorSequence(iterator).dropLast().reduce(initial, transform) + return Index(index: base.endIndex, previous: previous) + } + + public subscript(position: Index) -> Base.Element { + let element = base[position.index] + switch position.previous { + case .none: return element + case .some(let previous): return transform(previous, element) + } + } + + public func index(after i: Index) -> Index { + Index(index: base.index(after: i.index), previous: self[i]) + } + + public func distance(from start: Index, to end: Index) -> Int { + base.distance(from: start.index, to: end.index) + } +} + extension InclusiveReductions: LazySequenceProtocol where Base: LazySequenceProtocol {} + +extension InclusiveReductions: LazyCollectionProtocol + where Base: LazyCollectionProtocol {} diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index fe258619..94ce9291 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -18,10 +18,12 @@ final class ReductionsTests: XCTestCase { // MARK: - Exclusive Reductions - func testExclusiveLazySequence() { + func testExclusiveLazy() { XCTAssertEqualSequences((1...).prefix(4).lazy.reductions(0, +), [0, 1, 3, 6, 10]) XCTAssertEqualSequences((1...).prefix(1).lazy.reductions(0, +), [0, 1]) XCTAssertEqualSequences((1...).prefix(0).lazy.reductions(0, +), [0]) + + XCTAssertLazy([1].lazy.reductions(0, +)) } func testExclusiveEager() { @@ -35,10 +37,16 @@ final class ReductionsTests: XCTestCase { // MARK: - Inclusive Reductions - func testInclusiveLazySequence() { + func testInclusiveLazy() { XCTAssertEqualSequences((1...).prefix(4).lazy.reductions(+), [1, 3, 6, 10]) XCTAssertEqualSequences((1...).prefix(1).lazy.reductions(+), [1]) XCTAssertEqualSequences((1...).prefix(0).lazy.reductions(+), []) + + XCTAssertEqualCollections([1, 2, 3, 4].lazy.reductions(+), [1, 3, 6, 10]) + XCTAssertEqualCollections([1].lazy.reductions(+), [1]) + XCTAssertEqualCollections(EmptyCollection().lazy.reductions(+), []) + + XCTAssertLazy([1].lazy.reductions(+)) } func testInclusiveEager() { diff --git a/Tests/SwiftAlgorithmsTests/TestUtilities.swift b/Tests/SwiftAlgorithmsTests/TestUtilities.swift index 11520b93..b160caa6 100644 --- a/Tests/SwiftAlgorithmsTests/TestUtilities.swift +++ b/Tests/SwiftAlgorithmsTests/TestUtilities.swift @@ -68,6 +68,21 @@ func XCTAssertEqualSequences( func XCTAssertLazySequence(_: S) {} func XCTAssertLazyCollection(_: S) {} +/// Asserts two collections are equal by using their indices to access elements. +func XCTAssertEqualCollections( + _ expression1: @autoclosure () throws -> C1, + _ expression2: @autoclosure () throws -> C2, + _ message: @autoclosure () -> String = "", + file: StaticString = #file, line: UInt = #line +) rethrows where C1.Element: Equatable, C1.Element == C2.Element { + let c1 = try expression1() + let c2 = try expression2() + XCTAssertEqual(c1.indices.count, c2.indices.count, message(), file: file, line: line) + for index in zip(c1.indices, c2.indices) { + XCTAssertEqual(c1[index.0], c2[index.1], message(), file: file, line: line) + } +} + /// Tests that all index traversal methods behave as expected. /// /// Verifies the correctness of the implementations of `startIndex`, `endIndex`, From 0c60ad759e3488ee6cd5ef3366ade39d34c8574d Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Fri, 27 Nov 2020 16:49:53 +0000 Subject: [PATCH 21/45] Update guide --- Guides/Reductions.md | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/Guides/Reductions.md b/Guides/Reductions.md index dfd9405f..a86c5582 100644 --- a/Guides/Reductions.md +++ b/Guides/Reductions.md @@ -13,33 +13,42 @@ let runningTotal = (1...5).reductions(0, +) print(Array(runningTotal)) // prints [0, 1, 3, 6, 10, 15] -let runningMinimum = [3, 4, 2, 3, 1].reductions(.max, min) +let runningTotal = (1...5).reductions(+) +print(Array(runningTotal)) +// prints [1, 3, 6, 10, 15] + +let runningMinimum = [3, 4, 2, 3, 1].reductions(min) print(Array(runningMinimum)) // prints [3, 3, 2, 2, 1] ``` ## Detailed Design -One new method is added to sequences: +One pair of methods are added to `LazySequenceProtocol` for a lazily evaluated +sequence and another pair are added to `Sequence` which are eagerly evaluated. ```swift extension LazySequenceProtocol { - func reductions( - _ initial: Result, - _ transform: @escaping (Result, Element) -> Result - ) -> Reductions -} -extension Sequence { public func reductions( _ initial: Result, - _ transform: (Result, Element) throws -> Result - ) rethrows -> [Result] + _ transform: @escaping (Result, Element) -> Result + ) -> ExclusiveReductions + + public func reductions( + _ transform: @escaping (Element, Element) -> Element + ) -> InclusiveReductions } ``` ```swift -extension Collection { +extension Sequence { + + public func reductions( + _ initial: Result, + _ transform: (Result, Element) throws -> Result + ) rethrows -> [Result] + public func reductions( _ transform: (Element, Element) throws -> Element ) rethrows -> [Element] @@ -76,6 +85,13 @@ a good job at explaining the relation between the two. approachable—I feel like I can extrapolate the expected behavior purely from my familiarity with `reduce`. +As part of early discussions, it was decided to have two variants, one which +takes an initial value to use for the first element in the returned sequence, +and another which uses the first value of the base sequence as the initial +value. C++ calls these variants exclusive and inclusive respectively and so +these terms carry through as the name for the lazy sequences; +`ExclusiveReductions` and `InclusiveReductions`. + [SE-0045]: https://forums.swift.org/t/review-se-0045-add-scan-prefix-while-drop-while-and-iterate-to-the-stdlib/2382 [Issue 25]: https://github.com/apple/swift-algorithms/issues/25 [Brent_Royal-Gordon]: https://forums.swift.org/t/review-se-0045-add-scan-prefix-while-drop-while-and-iterate-to-the-stdlib/2382/6 From 2a41ecbb12e28e00b74fc8c2b0e668b87452e9da Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Fri, 27 Nov 2020 16:53:26 +0000 Subject: [PATCH 22/45] Add links for C++ implementations --- Guides/Reductions.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Guides/Reductions.md b/Guides/Reductions.md index a86c5582..00280dec 100644 --- a/Guides/Reductions.md +++ b/Guides/Reductions.md @@ -100,8 +100,9 @@ these terms carry through as the name for the lazy sequences; ### Comparison with other langauges -**C++:** As of C++17, the `` library includes an `inclusive_scan` -function. +**C++:** As of C++17, the `` library includes both +[`exclusive_scan`][C++ Exclusive] and [`inclusive_scan`][C++ Inclusive] +functions. **[Clojure][Clojure]:** Clojure 1.2 added a `reductions` function. @@ -114,6 +115,8 @@ parameter. **[Rust][Rust]:** Rust provides a `scan` function. +[C++ Exclusive]: https://en.cppreference.com/w/cpp/algorithm/exclusive_scan +[C++ Inclusive]: https://en.cppreference.com/w/cpp/algorithm/inclusive_scan [Clojure]: http://clojure.github.io/clojure/clojure.core-api.html#clojure.core/reductions [Haskell]: http://hackage.haskell.org/package/base-4.8.2.0/docs/Prelude.html#v:scanl [Rust]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.scan From aac96c348c2bcf48f149665e17229e0180095d68 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Sat, 28 Nov 2020 21:56:34 +0000 Subject: [PATCH 23/45] Add conformance to Collection for ExclusiveReductions --- Sources/Algorithms/Reductions.swift | 72 +++++++++++++++++++ .../ReductionsTests.swift | 4 ++ 2 files changed, 76 insertions(+) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 95335463..33ef533d 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -89,9 +89,81 @@ extension ExclusiveReductions: Sequence { } } +extension ExclusiveReductions: Collection where Base: Collection { + public struct Index: Comparable { + enum Representation { + case start + case base(index: Base.Index, previous: Result) + } + let representation: Representation + + public static func < (lhs: Index, rhs: Index) -> Bool { + switch (lhs.representation, rhs.representation) { + case (_, .start): return false + case (.start, _): return true + case let (.base(lhs, _), .base(rhs, _)): return lhs < rhs + } + } + + public static func == (lhs: Index, rhs: Index) -> Bool { + switch (lhs.representation, rhs.representation) { + case (.start, .start): return true + case let (.base(lhs, _), .base(rhs, _)): return lhs == rhs + default: return false + } + } + + static func base(index: Base.Index, previous: Result) -> Self { + Self(representation: .base(index: index, previous: previous)) + } + } + + public var startIndex: Index { + Index(representation: .start) + } + + public var endIndex: Index { + let previous = base.reduce(initial, transform) + return .base(index: base.endIndex, previous: previous) + } + + public subscript(position: Index) -> Result { + switch position.representation { + case .start: return initial + case let .base(index, previous): return transform(previous, base[index]) + } + } + + public func index(after i: Index) -> Index { + switch i.representation { + case .start: + return .base(index: base.startIndex, previous: initial) + case let .base(index, previous): + return .base(index: base.index(after: index), + previous: transform(previous, base[index])) + } + } + + public func distance(from start: Index, to end: Index) -> Int { + switch (start.representation, end.representation) { + case (.start, .start): + return 0 + case let (.start, .base(end, _)): + return base.distance(from: base.startIndex, to: end) + 1 + case let (.base(start, _), .base(end, _)): + return base.distance(from: start, to: end) + case let (.base(start, _), .start): + return base.distance(from: start, to: base.startIndex) - 1 + } + } +} + extension ExclusiveReductions: LazySequenceProtocol where Base: LazySequenceProtocol {} +extension ExclusiveReductions: LazyCollectionProtocol + where Base: LazyCollectionProtocol {} + // MARK: - Inclusive Reductions extension LazySequenceProtocol { diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index 94ce9291..88c59faf 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -23,6 +23,10 @@ final class ReductionsTests: XCTestCase { XCTAssertEqualSequences((1...).prefix(1).lazy.reductions(0, +), [0, 1]) XCTAssertEqualSequences((1...).prefix(0).lazy.reductions(0, +), [0]) + XCTAssertEqualCollections([1, 2, 3, 4].lazy.reductions(0, +), [0, 1, 3, 6, 10]) + XCTAssertEqualCollections([1].lazy.reductions(0, +), [0, 1]) + XCTAssertEqualCollections(EmptyCollection().lazy.reductions(0, +), [0]) + XCTAssertLazy([1].lazy.reductions(0, +)) } From a61878f508df8099a525b2fd339d17b3e2350299 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Sat, 28 Nov 2020 23:42:35 +0000 Subject: [PATCH 24/45] Improve ergonomics of ExclusiveReductions' index representation --- Sources/Algorithms/Reductions.swift | 54 ++++++++++++++++++----------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 33ef533d..8acd3c04 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -93,7 +93,8 @@ extension ExclusiveReductions: Collection where Base: Collection { public struct Index: Comparable { enum Representation { case start - case base(index: Base.Index, previous: Result) + case base(index: Base.Index, result: Result) + case end } let representation: Representation @@ -101,6 +102,8 @@ extension ExclusiveReductions: Collection where Base: Collection { switch (lhs.representation, rhs.representation) { case (_, .start): return false case (.start, _): return true + case (.end, _): return false + case (_, .end): return true case let (.base(lhs, _), .base(rhs, _)): return lhs < rhs } } @@ -108,39 +111,40 @@ extension ExclusiveReductions: Collection where Base: Collection { public static func == (lhs: Index, rhs: Index) -> Bool { switch (lhs.representation, rhs.representation) { case (.start, .start): return true + case (.end, .end): return true case let (.base(lhs, _), .base(rhs, _)): return lhs == rhs default: return false } } - static func base(index: Base.Index, previous: Result) -> Self { - Self(representation: .base(index: index, previous: previous)) + static func base(index: Base.Index, result: Result) -> Self { + Self(representation: .base(index: index, result: result)) } } - public var startIndex: Index { - Index(representation: .start) - } - - public var endIndex: Index { - let previous = base.reduce(initial, transform) - return .base(index: base.endIndex, previous: previous) - } + public var startIndex: Index { Index(representation: .start) } + public var endIndex: Index { Index(representation: .end) } public subscript(position: Index) -> Result { switch position.representation { case .start: return initial - case let .base(index, previous): return transform(previous, base[index]) + case .base(_, let result): return result + case .end: fatalError("Cannot get element of end index.") } } public func index(after i: Index) -> Index { + func index(base index: Base.Index, previous: Result) -> Index { + guard index != base.endIndex else { return endIndex } + return .base(index: index, result: transform(previous, base[index])) + } switch i.representation { case .start: - return .base(index: base.startIndex, previous: initial) - case let .base(index, previous): - return .base(index: base.index(after: index), - previous: transform(previous, base[index])) + return index(base: base.startIndex, previous: initial) + case let .base(i, result): + return index(base: base.index(after: i), previous: result) + case .end: + fatalError("Cannot get index after end index.") } } @@ -148,12 +152,22 @@ extension ExclusiveReductions: Collection where Base: Collection { switch (start.representation, end.representation) { case (.start, .start): return 0 - case let (.start, .base(end, _)): - return base.distance(from: base.startIndex, to: end) + 1 + case let (.start, .base(index, _)): + return base.distance(from: base.startIndex, to: index) + 1 + case (.start, .end): + return base.distance(from: base.startIndex, to: base.endIndex) + 1 + case let (.base(index, _), .start): + return base.distance(from: index, to: base.startIndex) - 1 case let (.base(start, _), .base(end, _)): return base.distance(from: start, to: end) - case let (.base(start, _), .start): - return base.distance(from: start, to: base.startIndex) - 1 + case let (.base(index, _), .end): + return base.distance(from: index, to: base.endIndex) + case (.end, .start): + return base.distance(from: base.endIndex, to: base.startIndex) - 1 + case let (.end, .base(index, _)): + return base.distance(from: base.endIndex, to: index) + case (.end, .end): + return 0 } } } From 058edc8fe56d47673176a5cb6d131e4075e76f29 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Sun, 29 Nov 2020 00:05:12 +0000 Subject: [PATCH 25/45] Improve ergonomics of InclusiveReductions' index representation by sharing the Index implementation --- Sources/Algorithms/Reductions.swift | 130 ++++++++++++++++------------ 1 file changed, 76 insertions(+), 54 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 8acd3c04..3d97e1f2 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -90,37 +90,7 @@ extension ExclusiveReductions: Sequence { } extension ExclusiveReductions: Collection where Base: Collection { - public struct Index: Comparable { - enum Representation { - case start - case base(index: Base.Index, result: Result) - case end - } - let representation: Representation - - public static func < (lhs: Index, rhs: Index) -> Bool { - switch (lhs.representation, rhs.representation) { - case (_, .start): return false - case (.start, _): return true - case (.end, _): return false - case (_, .end): return true - case let (.base(lhs, _), .base(rhs, _)): return lhs < rhs - } - } - - public static func == (lhs: Index, rhs: Index) -> Bool { - switch (lhs.representation, rhs.representation) { - case (.start, .start): return true - case (.end, .end): return true - case let (.base(lhs, _), .base(rhs, _)): return lhs == rhs - default: return false - } - } - - static func base(index: Base.Index, result: Result) -> Self { - Self(representation: .base(index: index, result: result)) - } - } + public typealias Index = ReductionsIndex public var startIndex: Index { Index(representation: .start) } public var endIndex: Index { Index(representation: .end) } @@ -228,44 +198,62 @@ extension InclusiveReductions: Sequence { } extension InclusiveReductions: Collection where Base: Collection { - public struct Index: Comparable { - let index: Base.Index - let previous: Element? - - public static func < (lhs: Index, rhs: Index) -> Bool { - lhs.index < rhs.index - } - - public static func == (lhs: Index, rhs: Index) -> Bool { - lhs.index == rhs.index - } - } + public typealias Index = ReductionsIndex public var startIndex: Index { - Index(index: base.startIndex, previous: nil) + guard base.startIndex != base.endIndex else { return endIndex } + return Index(representation: .start) } public var endIndex: Index { - var iterator = makeIterator() - guard let initial = iterator.next() else { return startIndex } - let previous = IteratorSequence(iterator).dropLast().reduce(initial, transform) - return Index(index: base.endIndex, previous: previous) + Index(representation: .end) } public subscript(position: Index) -> Base.Element { - let element = base[position.index] - switch position.previous { - case .none: return element - case .some(let previous): return transform(previous, element) + switch position.representation { + case .start: return base[base.startIndex] + case .base(_, let result): return result + case .end: fatalError("Cannot get element of end index.") } } public func index(after i: Index) -> Index { - Index(index: base.index(after: i.index), previous: self[i]) + func index(base index: Base.Index, previous: Base.Element) -> Index { + guard index != base.endIndex else { return endIndex } + return .base(index: index, result: transform(previous, base[index])) + } + switch i.representation { + case .start: + let i = base.startIndex + return index(base: base.index(after: i), previous: base[i]) + case let .base(i, element): + return index(base: base.index(after: i), previous: element) + case .end: + fatalError("Cannot get index after end index.") + } } public func distance(from start: Index, to end: Index) -> Int { - base.distance(from: start.index, to: end.index) + switch (start.representation, end.representation) { + case (.start, .start): + return 0 + case let (.start, .base(index, _)): + return base.distance(from: base.startIndex, to: index) + case (.start, .end): + return base.distance(from: base.startIndex, to: base.endIndex) + case let (.base(index, _), .start): + return base.distance(from: index, to: base.startIndex) + case let (.base(start, _), .base(end, _)): + return base.distance(from: start, to: end) + case let (.base(index, _), .end): + return base.distance(from: index, to: base.endIndex) + case (.end, .start): + return base.distance(from: base.endIndex, to: base.startIndex) + case let (.end, .base(index, _)): + return base.distance(from: base.endIndex, to: index) + case (.end, .end): + return 0 + } } } @@ -274,3 +262,37 @@ extension InclusiveReductions: LazySequenceProtocol extension InclusiveReductions: LazyCollectionProtocol where Base: LazyCollectionProtocol {} + +// MARK: - Shared ReductionsIndex + +public struct ReductionsIndex: Comparable { + enum Representation { + case start + case base(index: BaseIndex, result: Result) + case end + } + let representation: Representation + + public static func < (lhs: Self, rhs: Self) -> Bool { + switch (lhs.representation, rhs.representation) { + case (_, .start): return false + case (.start, _): return true + case (.end, _): return false + case (_, .end): return true + case let (.base(lhs, _), .base(rhs, _)): return lhs < rhs + } + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs.representation, rhs.representation) { + case (.start, .start): return true + case (.end, .end): return true + case let (.base(lhs, _), .base(rhs, _)): return lhs == rhs + default: return false + } + } + + static func base(index: BaseIndex, result: Result) -> Self { + Self(representation: .base(index: index, result: result)) + } +} From a2219970111821204ed4615abca09d5477eae4c7 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Sun, 29 Nov 2020 00:12:17 +0000 Subject: [PATCH 26/45] Tidy up internal function --- Sources/Algorithms/Reductions.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 3d97e1f2..749aaa90 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -218,16 +218,16 @@ extension InclusiveReductions: Collection where Base: Collection { } public func index(after i: Index) -> Index { - func index(base index: Base.Index, previous: Base.Element) -> Index { + func index(after i: Base.Index, previous: Base.Element) -> Index { + let index = base.index(after: i) guard index != base.endIndex else { return endIndex } return .base(index: index, result: transform(previous, base[index])) } switch i.representation { case .start: - let i = base.startIndex - return index(base: base.index(after: i), previous: base[i]) + return index(after: base.startIndex, previous: base[base.startIndex]) case let .base(i, element): - return index(base: base.index(after: i), previous: element) + return index(after: i, previous: element) case .end: fatalError("Cannot get index after end index.") } From ab40e33a68aadc49254af3dc80f9a7b1d3702ad9 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Sun, 29 Nov 2020 10:44:53 +0000 Subject: [PATCH 27/45] Add exclusive eager version of reductions(into:_:) --- Sources/Algorithms/Reductions.swift | 16 +++++++++++++--- Tests/SwiftAlgorithmsTests/ReductionsTests.swift | 8 ++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 749aaa90..f707ff2a 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -47,14 +47,24 @@ extension Sequence { _ transform: (Result, Element) throws -> Result ) rethrows -> [Result] { + var result = initial + return try reductions(into: &result) { result, element in + result = try transform(result, element) + } + } + + public func reductions( + into initial: inout Result, + _ transform: (inout Result, Element) throws -> Void + ) rethrows -> [Result] { + var output = [Result]() output.reserveCapacity(underestimatedCount + 1) output.append(initial) - var result = initial for element in self { - result = try transform(result, element) - output.append(result) + try transform(&initial, element) + output.append(initial) } return output diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index 88c59faf..08ed2f2d 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -35,6 +35,14 @@ final class ReductionsTests: XCTestCase { XCTAssertEqual([1].reductions(0, +), [0, 1]) XCTAssertEqual(EmptyCollection().reductions(0, +), [0]) + var value0 = 0 + var value1 = 0 + var value2 = 0 + func add(lhs: inout Int, rhs: Int) { lhs += rhs } + XCTAssertEqual([1, 2, 3, 4].reductions(into: &value0, add), [0, 1, 3, 6, 10]) + XCTAssertEqual([1].reductions(into: &value1, add), [0, 1]) + XCTAssertEqual(EmptyCollection().reductions(into: &value2, add), [0]) + XCTAssertNoThrow(try [].reductions(0) { _, _ in throw TestError() }) XCTAssertThrowsError(try [1].reductions(0) { _, _ in throw TestError() }) } From 6b017370f0744217a6985412ad137f96c0372489 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 3 Dec 2020 08:03:14 +0000 Subject: [PATCH 28/45] Use new lazy assertion functions --- Tests/SwiftAlgorithmsTests/ReductionsTests.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index 08ed2f2d..920a759d 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -27,7 +27,9 @@ final class ReductionsTests: XCTestCase { XCTAssertEqualCollections([1].lazy.reductions(0, +), [0, 1]) XCTAssertEqualCollections(EmptyCollection().lazy.reductions(0, +), [0]) - XCTAssertLazy([1].lazy.reductions(0, +)) + XCTAssertLazySequence((1...).prefix(1).lazy.reductions(0, +)) + XCTAssertLazySequence([1].lazy.reductions(0, +)) + XCTAssertLazyCollection([1].lazy.reductions(0, +)) } func testExclusiveEager() { @@ -58,7 +60,9 @@ final class ReductionsTests: XCTestCase { XCTAssertEqualCollections([1].lazy.reductions(+), [1]) XCTAssertEqualCollections(EmptyCollection().lazy.reductions(+), []) - XCTAssertLazy([1].lazy.reductions(+)) + XCTAssertLazySequence((1...).prefix(1).lazy.reductions(+)) + XCTAssertLazySequence([1].lazy.reductions(+)) + XCTAssertLazyCollection([1].lazy.reductions(+)) } func testInclusiveEager() { From 714f46bc63984947a32c910d2afd003215fde1af Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 25 Feb 2021 08:32:21 +0000 Subject: [PATCH 29/45] Correct the complexity claims for reductions --- Guides/Reductions.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Guides/Reductions.md b/Guides/Reductions.md index 00280dec..f362230c 100644 --- a/Guides/Reductions.md +++ b/Guides/Reductions.md @@ -24,7 +24,7 @@ print(Array(runningMinimum)) ## Detailed Design -One pair of methods are added to `LazySequenceProtocol` for a lazily evaluated +One pair of methods are added to `LazySequenceProtocol` for a lazily evaluated. sequence and another pair are added to `Sequence` which are eagerly evaluated. ```swift @@ -57,7 +57,8 @@ extension Sequence { ### Complexity -Calling these methods is O(_1_). +Calling the lazy methods, those defined on `LazySequenceProtocol`, is O(_1_). +Calling the eager methods, those returning an array, is O(_n_). ### Naming From 5182363625df0bfaaa1db5be9d5ec35eaa699acc Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 25 Feb 2021 08:51:04 +0000 Subject: [PATCH 30/45] Separate the index types This will allow for future changes to either implementation without causing a breakage for the other. --- Sources/Algorithms/Reductions.swift | 59 ++++++++++++++++++----------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index f707ff2a..38fa1ad3 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -100,7 +100,15 @@ extension ExclusiveReductions: Sequence { } extension ExclusiveReductions: Collection where Base: Collection { - public typealias Index = ReductionsIndex + public struct Index: Comparable { + let representation: ReductionsIndexRepresentation + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.representation < rhs.representation + } + static func base(index: Base.Index, result: Result) -> Self { + Self(representation: .base(index: index, result: result)) + } + } public var startIndex: Index { Index(representation: .start) } public var endIndex: Index { Index(representation: .end) } @@ -208,7 +216,15 @@ extension InclusiveReductions: Sequence { } extension InclusiveReductions: Collection where Base: Collection { - public typealias Index = ReductionsIndex + public struct Index: Comparable { + let representation: ReductionsIndexRepresentation + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.representation < rhs.representation + } + static func base(index: Base.Index, result: Base.Element) -> Self { + Self(representation: .base(index: index, result: result)) + } + } public var startIndex: Index { guard base.startIndex != base.endIndex else { return endIndex } @@ -273,36 +289,33 @@ extension InclusiveReductions: LazySequenceProtocol extension InclusiveReductions: LazyCollectionProtocol where Base: LazyCollectionProtocol {} -// MARK: - Shared ReductionsIndex +// MARK: - ReductionsIndexRepresentation -public struct ReductionsIndex: Comparable { - enum Representation { - case start - case base(index: BaseIndex, result: Result) - case end - } - let representation: Representation - - public static func < (lhs: Self, rhs: Self) -> Bool { - switch (lhs.representation, rhs.representation) { - case (_, .start): return false - case (.start, _): return true - case (.end, _): return false - case (_, .end): return true - case let (.base(lhs, _), .base(rhs, _)): return lhs < rhs - } - } +enum ReductionsIndexRepresentation { + case start + case base(index: BaseIndex, result: Result) + case end +} +extension ReductionsIndexRepresentation: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { - switch (lhs.representation, rhs.representation) { + switch (lhs, rhs) { case (.start, .start): return true case (.end, .end): return true case let (.base(lhs, _), .base(rhs, _)): return lhs == rhs default: return false } } +} - static func base(index: BaseIndex, result: Result) -> Self { - Self(representation: .base(index: index, result: result)) +extension ReductionsIndexRepresentation: Comparable { + public static func < (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (_, .start): return false + case (.start, _): return true + case (.end, _): return false + case (_, .end): return true + case let (.base(lhs, _), .base(rhs, _)): return lhs < rhs + } } } From 84b6ddc05a721a5f224565ca583614041fbeb213 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Mon, 8 Mar 2021 08:32:55 +0000 Subject: [PATCH 31/45] Update guide to reflect that arrays are returned directly --- Guides/Reductions.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Guides/Reductions.md b/Guides/Reductions.md index f362230c..6f964c03 100644 --- a/Guides/Reductions.md +++ b/Guides/Reductions.md @@ -9,16 +9,16 @@ This has the behaviour of reduce, but instead of returning the final result value, it returns the a sequence of the results returned from each element. ```swift -let runningTotal = (1...5).reductions(0, +) -print(Array(runningTotal)) +let runningTotalExclusive = (1...5).reductions(0, +) +print(runningTotalExclusive) // prints [0, 1, 3, 6, 10, 15] -let runningTotal = (1...5).reductions(+) -print(Array(runningTotal)) +let runningTotalInclusive = (1...5).reductions(+) +print(runningTotalInclusive) // prints [1, 3, 6, 10, 15] let runningMinimum = [3, 4, 2, 3, 1].reductions(min) -print(Array(runningMinimum)) +print(runningMinimum) // prints [3, 3, 2, 2, 1] ``` From fb004f7611cf2f6e1039947d2bd284c4e5a8f133 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Mon, 8 Mar 2021 08:43:18 +0000 Subject: [PATCH 32/45] Add scan as deprecated methods --- Sources/Algorithms/Reductions.swift | 52 +++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 38fa1ad3..a5e7fcde 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -319,3 +319,55 @@ extension ReductionsIndexRepresentation: Comparable { } } } + +// MARK: - Scan + +extension LazySequenceProtocol { + + @available(*, deprecated, message: "Use reductions(_:_:) instead.") + public func scan( + _ initial: Result, + _ transform: @escaping (Result, Element) -> Result + ) -> ExclusiveReductions { + reductions(initial, transform) + } +} + +extension Sequence { + + @available(*, deprecated, message: "Use reductions(_:_:) instead.") + public func scan( + _ initial: Result, + _ transform: (Result, Element) throws -> Result + ) rethrows -> [Result] { + try reductions(initial, transform) + } + + @available(*, deprecated, message: "Use reductions(into:_:) instead.") + public func scan( + into initial: inout Result, + _ transform: (inout Result, Element) throws -> Void + ) rethrows -> [Result] { + try reductions(into: &initial, transform) + } +} + +extension LazySequenceProtocol { + + @available(*, deprecated, message: "Use reductions(_:) instead.") + public func scan( + _ transform: @escaping (Element, Element) -> Element + ) -> InclusiveReductions { + reductions(transform) + } +} + +extension Sequence { + + @available(*, deprecated, message: "Use reductions(_:) instead.") + public func scan( + _ transform: (Element, Element) throws -> Element + ) rethrows -> [Element] { + try reductions(transform) + } +} From 7464876ef9c6c6edfe6b93d2d8469d53cd4b3b28 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 18 Mar 2021 16:47:46 +0000 Subject: [PATCH 33/45] Add lazy overload of reductions(into:_:) --- Sources/Algorithms/Reductions.swift | 33 ++++++++++++++++--- .../ReductionsTests.swift | 8 +++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index a5e7fcde..39f540ae 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -36,6 +36,17 @@ extension LazySequenceProtocol { _ initial: Result, _ transform: @escaping (Result, Element) -> Result ) -> ExclusiveReductions { + + var result = initial + return reductions(into: &result) { result, element in + result = transform(result, element) + } + } + + public func reductions( + into initial: inout Result, + _ transform: @escaping (inout Result, Element) -> Void + ) -> ExclusiveReductions { ExclusiveReductions(base: self, initial: initial, transform: transform) } } @@ -76,18 +87,22 @@ extension Sequence { public struct ExclusiveReductions { let base: Base let initial: Result - let transform: (Result, Base.Element) -> Result + let transform: (inout Result, Base.Element) -> Void } extension ExclusiveReductions: Sequence { public struct Iterator: IteratorProtocol { var iterator: Base.Iterator var current: Result? - let transform: (Result, Base.Element) -> Result + let transform: (inout Result, Base.Element) -> Void public mutating func next() -> Result? { guard let result = current else { return nil } - current = iterator.next().map { transform(result, $0) } + current = iterator.next().map { element in + var result = result + transform(&result, element) + return result + } return result } } @@ -124,7 +139,9 @@ extension ExclusiveReductions: Collection where Base: Collection { public func index(after i: Index) -> Index { func index(base index: Base.Index, previous: Result) -> Index { guard index != base.endIndex else { return endIndex } - return .base(index: index, result: transform(previous, base[index])) + var previous = previous + transform(&previous, base[index]) + return .base(index: index, result: previous) } switch i.representation { case .start: @@ -331,6 +348,14 @@ extension LazySequenceProtocol { ) -> ExclusiveReductions { reductions(initial, transform) } + + @available(*, deprecated, message: "Use reductions(into:_:) instead.") + public func scan( + into initial: inout Result, + _ transform: @escaping (inout Result, Element) -> Void + ) -> ExclusiveReductions { + reductions(into: &initial, transform) + } } extension Sequence { diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index 920a759d..0f57b56b 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -27,6 +27,14 @@ final class ReductionsTests: XCTestCase { XCTAssertEqualCollections([1].lazy.reductions(0, +), [0, 1]) XCTAssertEqualCollections(EmptyCollection().lazy.reductions(0, +), [0]) + var value0 = 0 + var value1 = 0 + var value2 = 0 + func add(lhs: inout Int, rhs: Int) { lhs += rhs } + XCTAssertEqual([1, 2, 3, 4].lazy.reductions(into: &value0, add), [0, 1, 3, 6, 10]) + XCTAssertEqual([1].lazy.reductions(into: &value1, add), [0, 1]) + XCTAssertEqual(EmptyCollection().lazy.reductions(into: &value2, add), [0]) + XCTAssertLazySequence((1...).prefix(1).lazy.reductions(0, +)) XCTAssertLazySequence([1].lazy.reductions(0, +)) XCTAssertLazyCollection([1].lazy.reductions(0, +)) From 174efb8c167ef28dbd3ffa3e3583f4c4275fc7a5 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 18 Mar 2021 19:10:27 +0000 Subject: [PATCH 34/45] Add the reductions(into:_:) functions --- Guides/Reductions.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Guides/Reductions.md b/Guides/Reductions.md index 6f964c03..78b2f23e 100644 --- a/Guides/Reductions.md +++ b/Guides/Reductions.md @@ -24,8 +24,8 @@ print(runningMinimum) ## Detailed Design -One pair of methods are added to `LazySequenceProtocol` for a lazily evaluated. -sequence and another pair are added to `Sequence` which are eagerly evaluated. +One trio of methods are added to `LazySequenceProtocol` for a lazily evaluated +sequence and another trio are added to `Sequence` which are eagerly evaluated. ```swift extension LazySequenceProtocol { @@ -35,6 +35,11 @@ extension LazySequenceProtocol { _ transform: @escaping (Result, Element) -> Result ) -> ExclusiveReductions + public func reductions( + into initial: inout Result, + _ transform: @escaping (inout Result, Element) -> Void + ) -> ExclusiveReductions + public func reductions( _ transform: @escaping (Element, Element) -> Element ) -> InclusiveReductions @@ -48,7 +53,12 @@ extension Sequence { _ initial: Result, _ transform: (Result, Element) throws -> Result ) rethrows -> [Result] - + + public func reductions( + into initial: inout Result, + _ transform: (inout Result, Element) throws -> Void + ) rethrows -> [Result] + public func reductions( _ transform: (Element, Element) throws -> Element ) rethrows -> [Element] From 1e47f860564036ea7cb05c21c2095fe176dab835 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 18 Mar 2021 19:14:36 +0000 Subject: [PATCH 35/45] More succinctly introduce reductions --- Guides/Reductions.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Guides/Reductions.md b/Guides/Reductions.md index 78b2f23e..f9a4d3fa 100644 --- a/Guides/Reductions.md +++ b/Guides/Reductions.md @@ -5,8 +5,7 @@ Produces a sequence of values. -This has the behaviour of reduce, but instead of returning the final result -value, it returns the a sequence of the results returned from each element. +This has the behaviour of reduce, but also returns all intermediate results. ```swift let runningTotalExclusive = (1...5).reductions(0, +) From a73c6818d5c52d966e2ec2deda848a4043e4c987 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 18 Mar 2021 19:21:43 +0000 Subject: [PATCH 36/45] Add a note about the deprecated scan functions --- Guides/Reductions.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Guides/Reductions.md b/Guides/Reductions.md index f9a4d3fa..87e6bf61 100644 --- a/Guides/Reductions.md +++ b/Guides/Reductions.md @@ -73,7 +73,11 @@ Calling the eager methods, those returning an array, is O(_n_). While the name `scan` is the term of art for this function, it has been discussed that `reductions` aligns better with the existing `reduce` function -and will aid newcomers that might not know the existing `scan` term. +and will aid newcomers that might not know the existing `scan` term. + +Deprecated `scan` methods have been added for people who are familiar with the +term, so they can easily discover the `reductions` methods via compiler +deprecation warnings. Below are two quotes from the Swift forum [discussion about SE-0045][SE-0045] which proposed adding `scan` to the standard library and one from From f6b42ad18f95f881db7d2c5145feb3893d0676b0 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 18 Mar 2021 19:37:44 +0000 Subject: [PATCH 37/45] Update documentation --- Sources/Algorithms/Reductions.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 39f540ae..8f71f195 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -20,9 +20,8 @@ extension LazySequenceProtocol { /// providing the initial value followed by these results as a sequence. /// /// ``` - /// let values = [1, 2, 3, 4] - /// let sequence = values.reductions(0, +) - /// print(Array(sequence)) + /// let runningTotal = [1, 2, 3, 4].lazy.reductions(0, +) + /// print(Array(runningTotal)) /// /// // prints [0, 1, 3, 6, 10] /// ``` From f7b90b5d902b129673191e8f94aa95a673937108 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Thu, 18 Mar 2021 20:01:48 +0000 Subject: [PATCH 38/45] Add @inlinable and @usableFromInline --- Sources/Algorithms/Reductions.swift | 157 ++++++++++++++++++++++++---- 1 file changed, 134 insertions(+), 23 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 8f71f195..bde86857 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -31,6 +31,7 @@ extension LazySequenceProtocol { /// - transform: A closure that combines the previously reduced result and /// the next element in the receiving sequence. /// - Returns: A sequence of transformed elements. + @inlinable public func reductions( _ initial: Result, _ transform: @escaping (Result, Element) -> Result @@ -42,6 +43,7 @@ extension LazySequenceProtocol { } } + @inlinable public func reductions( into initial: inout Result, _ transform: @escaping (inout Result, Element) -> Void @@ -52,6 +54,7 @@ extension LazySequenceProtocol { extension Sequence { + @inlinable public func reductions( _ initial: Result, _ transform: (Result, Element) throws -> Result @@ -63,6 +66,7 @@ extension Sequence { } } + @inlinable public func reductions( into initial: inout Result, _ transform: (inout Result, Element) throws -> Void @@ -84,17 +88,50 @@ extension Sequence { /// A sequence of applying a transform to the element of a sequence and the /// previously transformed result. public struct ExclusiveReductions { - let base: Base - let initial: Result - let transform: (inout Result, Base.Element) -> Void + @usableFromInline + internal let base: Base + + @usableFromInline + internal let initial: Result + + @usableFromInline + internal let transform: (inout Result, Base.Element) -> Void + + @usableFromInline + internal init( + base: Base, + initial: Result, + transform: @escaping (inout Result, Base.Element) -> Void + ) { + self.base = base + self.initial = initial + self.transform = transform + } } extension ExclusiveReductions: Sequence { public struct Iterator: IteratorProtocol { - var iterator: Base.Iterator - var current: Result? - let transform: (inout Result, Base.Element) -> Void + @usableFromInline + internal var iterator: Base.Iterator + + @usableFromInline + internal var current: Result? + + @usableFromInline + internal let transform: (inout Result, Base.Element) -> Void + + @usableFromInline + internal init( + iterator: Base.Iterator, + current: Result? = nil, + transform: @escaping (inout Result, Base.Element) -> Void + ) { + self.iterator = iterator + self.current = current + self.transform = transform + } + @inlinable public mutating func next() -> Result? { guard let result = current else { return nil } current = iterator.next().map { element in @@ -106,6 +143,7 @@ extension ExclusiveReductions: Sequence { } } + @inlinable public func makeIterator() -> Iterator { Iterator(iterator: base.makeIterator(), current: initial, @@ -115,18 +153,33 @@ extension ExclusiveReductions: Sequence { extension ExclusiveReductions: Collection where Base: Collection { public struct Index: Comparable { - let representation: ReductionsIndexRepresentation + @usableFromInline + internal let representation: ReductionsIndexRepresentation + + @inlinable public static func < (lhs: Self, rhs: Self) -> Bool { lhs.representation < rhs.representation } - static func base(index: Base.Index, result: Result) -> Self { + + @usableFromInline + internal static func base(index: Base.Index, result: Result) -> Self { Self(representation: .base(index: index, result: result)) } + + @usableFromInline + internal static var start: Self { Self(representation: .start) } + + @usableFromInline + internal static var end: Self { Self(representation: .end) } } - public var startIndex: Index { Index(representation: .start) } - public var endIndex: Index { Index(representation: .end) } + @inlinable + public var startIndex: Index { .start } + @inlinable + public var endIndex: Index { .end } + + @inlinable public subscript(position: Index) -> Result { switch position.representation { case .start: return initial @@ -135,6 +188,7 @@ extension ExclusiveReductions: Collection where Base: Collection { } } + @inlinable public func index(after i: Index) -> Index { func index(base index: Base.Index, previous: Result) -> Index { guard index != base.endIndex else { return endIndex } @@ -152,6 +206,7 @@ extension ExclusiveReductions: Collection where Base: Collection { } } + @inlinable public func distance(from start: Index, to end: Index) -> Int { switch (start.representation, end.representation) { case (.start, .start): @@ -186,6 +241,7 @@ extension ExclusiveReductions: LazyCollectionProtocol extension LazySequenceProtocol { + @inlinable public func reductions( _ transform: @escaping (Element, Element) -> Element ) -> InclusiveReductions { @@ -194,6 +250,8 @@ extension LazySequenceProtocol { } extension Sequence { + + @inlinable public func reductions( _ transform: (Element, Element) throws -> Element ) rethrows -> [Element] { @@ -204,16 +262,45 @@ extension Sequence { } public struct InclusiveReductions { - let base: Base - let transform: (Base.Element, Base.Element) -> Base.Element + @usableFromInline + internal let base: Base + + @usableFromInline + internal let transform: (Base.Element, Base.Element) -> Base.Element + + @usableFromInline + internal init( + base: Base, + transform: @escaping (Base.Element, Base.Element) -> Base.Element + ) { + self.base = base + self.transform = transform + } } extension InclusiveReductions: Sequence { public struct Iterator: IteratorProtocol { - var iterator: Base.Iterator - var element: Base.Element? - let transform: (Base.Element, Base.Element) -> Base.Element + @usableFromInline + internal var iterator: Base.Iterator + + @usableFromInline + internal var element: Base.Element? + + @usableFromInline + internal let transform: (Base.Element, Base.Element) -> Base.Element + + @usableFromInline + internal init( + iterator: Base.Iterator, + element: Base.Element? = nil, + transform: @escaping (Base.Element, Base.Element) -> Base.Element + ) { + self.iterator = iterator + self.element = element + self.transform = transform + } + @inlinable public mutating func next() -> Base.Element? { guard let previous = element else { element = iterator.next() @@ -225,6 +312,7 @@ extension InclusiveReductions: Sequence { } } + @inlinable public func makeIterator() -> Iterator { Iterator(iterator: base.makeIterator(), transform: transform) @@ -233,24 +321,36 @@ extension InclusiveReductions: Sequence { extension InclusiveReductions: Collection where Base: Collection { public struct Index: Comparable { - let representation: ReductionsIndexRepresentation + @usableFromInline + internal let representation: ReductionsIndexRepresentation + + @inlinable public static func < (lhs: Self, rhs: Self) -> Bool { lhs.representation < rhs.representation } - static func base(index: Base.Index, result: Base.Element) -> Self { + + @usableFromInline + internal static func base(index: Base.Index, result: Base.Element) -> Self { Self(representation: .base(index: index, result: result)) } + + @usableFromInline + internal static var start: Self { Self(representation: .start) } + + @usableFromInline + internal static var end: Self { Self(representation: .end) } } + @inlinable public var startIndex: Index { guard base.startIndex != base.endIndex else { return endIndex } - return Index(representation: .start) + return .start } - public var endIndex: Index { - Index(representation: .end) - } + @inlinable + public var endIndex: Index { .end } + @inlinable public subscript(position: Index) -> Base.Element { switch position.representation { case .start: return base[base.startIndex] @@ -259,6 +359,7 @@ extension InclusiveReductions: Collection where Base: Collection { } } + @inlinable public func index(after i: Index) -> Index { func index(after i: Base.Index, previous: Base.Element) -> Index { let index = base.index(after: i) @@ -275,6 +376,7 @@ extension InclusiveReductions: Collection where Base: Collection { } } + @inlinable public func distance(from start: Index, to end: Index) -> Int { switch (start.representation, end.representation) { case (.start, .start): @@ -307,6 +409,7 @@ extension InclusiveReductions: LazyCollectionProtocol // MARK: - ReductionsIndexRepresentation +@usableFromInline enum ReductionsIndexRepresentation { case start case base(index: BaseIndex, result: Result) @@ -314,7 +417,8 @@ enum ReductionsIndexRepresentation { } extension ReductionsIndexRepresentation: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { + @usableFromInline + static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { case (.start, .start): return true case (.end, .end): return true @@ -325,7 +429,8 @@ extension ReductionsIndexRepresentation: Equatable { } extension ReductionsIndexRepresentation: Comparable { - public static func < (lhs: Self, rhs: Self) -> Bool { + @usableFromInline + static func < (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { case (_, .start): return false case (.start, _): return true @@ -341,6 +446,7 @@ extension ReductionsIndexRepresentation: Comparable { extension LazySequenceProtocol { @available(*, deprecated, message: "Use reductions(_:_:) instead.") + @inlinable public func scan( _ initial: Result, _ transform: @escaping (Result, Element) -> Result @@ -349,6 +455,7 @@ extension LazySequenceProtocol { } @available(*, deprecated, message: "Use reductions(into:_:) instead.") + @inlinable public func scan( into initial: inout Result, _ transform: @escaping (inout Result, Element) -> Void @@ -360,6 +467,7 @@ extension LazySequenceProtocol { extension Sequence { @available(*, deprecated, message: "Use reductions(_:_:) instead.") + @inlinable public func scan( _ initial: Result, _ transform: (Result, Element) throws -> Result @@ -368,6 +476,7 @@ extension Sequence { } @available(*, deprecated, message: "Use reductions(into:_:) instead.") + @inlinable public func scan( into initial: inout Result, _ transform: (inout Result, Element) throws -> Void @@ -379,6 +488,7 @@ extension Sequence { extension LazySequenceProtocol { @available(*, deprecated, message: "Use reductions(_:) instead.") + @inlinable public func scan( _ transform: @escaping (Element, Element) -> Element ) -> InclusiveReductions { @@ -389,6 +499,7 @@ extension LazySequenceProtocol { extension Sequence { @available(*, deprecated, message: "Use reductions(_:) instead.") + @inlinable public func scan( _ transform: (Element, Element) throws -> Element ) rethrows -> [Element] { From e9abcdb68e0e4a98933b372700422e3ef43d7567 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Fri, 19 Mar 2021 12:02:47 +0000 Subject: [PATCH 39/45] Copy documentation to all variants of reductions --- Sources/Algorithms/Reductions.swift | 104 +++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index bde86857..4a395320 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -13,8 +13,8 @@ extension LazySequenceProtocol { - /// Returns a sequence containing the results of combining the elements of - /// the sequence using the given transform. + /// Returns a sequence containing the accumulated results of combining the + /// elements of the sequence using the given closure. /// /// This can be seen as applying the reduce function to each element and /// providing the initial value followed by these results as a sequence. @@ -31,6 +31,8 @@ extension LazySequenceProtocol { /// - transform: A closure that combines the previously reduced result and /// the next element in the receiving sequence. /// - Returns: A sequence of transformed elements. + /// + /// - Complexity: O(1) @inlinable public func reductions( _ initial: Result, @@ -43,6 +45,26 @@ extension LazySequenceProtocol { } } + /// Returns a sequence containing the accumulated results of combining the + /// elements of the sequence using the given closure. + /// + /// This can be seen as applying the reduce function to each element and + /// providing the initial value followed by these results as a sequence. + /// + /// ``` + /// let runningTotal = [1, 2, 3, 4].lazy.reductions(into: 0, +) + /// print(Array(runningTotal)) + /// + /// // prints [0, 1, 3, 6, 10] + /// ``` + /// + /// - Parameters: + /// - initial: The value to use as the initial value. + /// - transform: A closure that combines the previously reduced result and + /// the next element in the receiving sequence. + /// - Returns: A sequence of transformed elements. + /// + /// - Complexity: O(1) @inlinable public func reductions( into initial: inout Result, @@ -54,6 +76,26 @@ extension LazySequenceProtocol { extension Sequence { + /// Returns an array containing the accumulated results of combining the + /// elements of the sequence using the given closure. + /// + /// This can be seen as applying the reduce function to each element and + /// providing the initial value followed by these results as a sequence. + /// + /// ``` + /// let runningTotal = [1, 2, 3, 4].reductions(0, +) + /// print(runningTotal) + /// + /// // prints [0, 1, 3, 6, 10] + /// ``` + /// + /// - Parameters: + /// - initial: The value to use as the initial value. + /// - transform: A closure that combines the previously reduced result and + /// the next element in the receiving sequence. + /// - Returns: An array of transformed elements. + /// + /// - Complexity: O(n) @inlinable public func reductions( _ initial: Result, @@ -66,6 +108,26 @@ extension Sequence { } } + /// Returns an array containing the accumulated results of combining the + /// elements of the sequence using the given closure. + /// + /// This can be seen as applying the reduce function to each element and + /// providing the initial value followed by these results as a sequence. + /// + /// ``` + /// let runningTotal = [1, 2, 3, 4].reductions(into: 0, +) + /// print(runningTotal) + /// + /// // prints [0, 1, 3, 6, 10] + /// ``` + /// + /// - Parameters: + /// - initial: The value to use as the initial value. + /// - transform: A closure that combines the previously reduced result and + /// the next element in the receiving sequence. + /// - Returns: An array of transformed elements. + /// + /// - Complexity: O(n) @inlinable public func reductions( into initial: inout Result, @@ -241,6 +303,25 @@ extension ExclusiveReductions: LazyCollectionProtocol extension LazySequenceProtocol { + /// Returns a sequence containing the accumulated results of combining the + /// elements of the sequence using the given closure. + /// + /// This can be seen as applying the reduce function to each element and + /// providing the initial value followed by these results as a sequence. + /// + /// ``` + /// let runningTotal = [1, 2, 3, 4].lazy.reductions(+) + /// print(Array(runningTotal)) + /// + /// // prints [1, 3, 6, 10] + /// ``` + /// + /// - Parameters: + /// - transform: A closure that combines the previously reduced result and + /// the next element in the receiving sequence. + /// - Returns: An array of accumulated elements. + /// + /// - Complexity: O(1) @inlinable public func reductions( _ transform: @escaping (Element, Element) -> Element @@ -251,6 +332,25 @@ extension LazySequenceProtocol { extension Sequence { + /// Returns an array containing the accumulated results of combining the + /// elements of the sequence using the given closure. + /// + /// This can be seen as applying the reduce function to each element and + /// providing the initial value followed by these results as a sequence. + /// + /// ``` + /// let runningTotal = [1, 2, 3, 4].reductions(+) + /// print(runningTotal) + /// + /// // prints [1, 3, 6, 10] + /// ``` + /// + /// - Parameters: + /// - transform: A closure that combines the previously reduced result and + /// the next element in the receiving sequence. + /// - Returns: An array of accumulated elements. + /// + /// - Complexity: O(n) @inlinable public func reductions( _ transform: (Element, Element) throws -> Element From 04f0a23270f1e0a69b25d6aeebb184ed09950779 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Fri, 19 Mar 2021 12:32:09 +0000 Subject: [PATCH 40/45] Test the value after the function is complete --- .../ReductionsTests.swift | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index 0f57b56b..2679da0b 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -16,6 +16,8 @@ final class ReductionsTests: XCTestCase { struct TestError: Error {} + func add(lhs: inout Int, rhs: Int) { lhs += rhs } + // MARK: - Exclusive Reductions func testExclusiveLazy() { @@ -27,13 +29,17 @@ final class ReductionsTests: XCTestCase { XCTAssertEqualCollections([1].lazy.reductions(0, +), [0, 1]) XCTAssertEqualCollections(EmptyCollection().lazy.reductions(0, +), [0]) - var value0 = 0 - var value1 = 0 - var value2 = 0 - func add(lhs: inout Int, rhs: Int) { lhs += rhs } - XCTAssertEqual([1, 2, 3, 4].lazy.reductions(into: &value0, add), [0, 1, 3, 6, 10]) - XCTAssertEqual([1].lazy.reductions(into: &value1, add), [0, 1]) - XCTAssertEqual(EmptyCollection().lazy.reductions(into: &value2, add), [0]) + var value = 0 + XCTAssertEqual([1, 2, 3, 4].lazy.reductions(into: &value, add), [0, 1, 3, 6, 10]) + XCTAssertEqual(value, 10) + + value = 0 + XCTAssertEqual([1].lazy.reductions(into: &value, add), [0, 1]) + XCTAssertEqual(value, 1) + + value = 0 + XCTAssertEqual(EmptyCollection().lazy.reductions(into: &value, add), [0]) + XCTAssertEqual(value, 0) XCTAssertLazySequence((1...).prefix(1).lazy.reductions(0, +)) XCTAssertLazySequence([1].lazy.reductions(0, +)) @@ -45,13 +51,17 @@ final class ReductionsTests: XCTestCase { XCTAssertEqual([1].reductions(0, +), [0, 1]) XCTAssertEqual(EmptyCollection().reductions(0, +), [0]) - var value0 = 0 - var value1 = 0 - var value2 = 0 - func add(lhs: inout Int, rhs: Int) { lhs += rhs } - XCTAssertEqual([1, 2, 3, 4].reductions(into: &value0, add), [0, 1, 3, 6, 10]) - XCTAssertEqual([1].reductions(into: &value1, add), [0, 1]) - XCTAssertEqual(EmptyCollection().reductions(into: &value2, add), [0]) + var value = 0 + XCTAssertEqual([1, 2, 3, 4].reductions(into: &value, add), [0, 1, 3, 6, 10]) + XCTAssertEqual(value, 10) + + value = 0 + XCTAssertEqual([1].reductions(into: &value, add), [0, 1]) + XCTAssertEqual(value, 1) + + value = 0 + XCTAssertEqual(EmptyCollection().reductions(into: &value, add), [0]) + XCTAssertEqual(value, 0) XCTAssertNoThrow(try [].reductions(0) { _, _ in throw TestError() }) XCTAssertThrowsError(try [1].reductions(0) { _, _ in throw TestError() }) From 724e22318ffd17a76a4b9bda6968e7b8fc6adcff Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Fri, 19 Mar 2021 12:32:49 +0000 Subject: [PATCH 41/45] Just use the += operator --- Tests/SwiftAlgorithmsTests/ReductionsTests.swift | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift index 2679da0b..60a53709 100644 --- a/Tests/SwiftAlgorithmsTests/ReductionsTests.swift +++ b/Tests/SwiftAlgorithmsTests/ReductionsTests.swift @@ -16,8 +16,6 @@ final class ReductionsTests: XCTestCase { struct TestError: Error {} - func add(lhs: inout Int, rhs: Int) { lhs += rhs } - // MARK: - Exclusive Reductions func testExclusiveLazy() { @@ -30,15 +28,15 @@ final class ReductionsTests: XCTestCase { XCTAssertEqualCollections(EmptyCollection().lazy.reductions(0, +), [0]) var value = 0 - XCTAssertEqual([1, 2, 3, 4].lazy.reductions(into: &value, add), [0, 1, 3, 6, 10]) + XCTAssertEqual([1, 2, 3, 4].lazy.reductions(into: &value, +=), [0, 1, 3, 6, 10]) XCTAssertEqual(value, 10) value = 0 - XCTAssertEqual([1].lazy.reductions(into: &value, add), [0, 1]) + XCTAssertEqual([1].lazy.reductions(into: &value, +=), [0, 1]) XCTAssertEqual(value, 1) value = 0 - XCTAssertEqual(EmptyCollection().lazy.reductions(into: &value, add), [0]) + XCTAssertEqual(EmptyCollection().lazy.reductions(into: &value, +=), [0]) XCTAssertEqual(value, 0) XCTAssertLazySequence((1...).prefix(1).lazy.reductions(0, +)) @@ -52,15 +50,15 @@ final class ReductionsTests: XCTestCase { XCTAssertEqual(EmptyCollection().reductions(0, +), [0]) var value = 0 - XCTAssertEqual([1, 2, 3, 4].reductions(into: &value, add), [0, 1, 3, 6, 10]) + XCTAssertEqual([1, 2, 3, 4].reductions(into: &value, +=), [0, 1, 3, 6, 10]) XCTAssertEqual(value, 10) value = 0 - XCTAssertEqual([1].reductions(into: &value, add), [0, 1]) + XCTAssertEqual([1].reductions(into: &value, +=), [0, 1]) XCTAssertEqual(value, 1) value = 0 - XCTAssertEqual(EmptyCollection().reductions(into: &value, add), [0]) + XCTAssertEqual(EmptyCollection().reductions(into: &value, +=), [0]) XCTAssertEqual(value, 0) XCTAssertNoThrow(try [].reductions(0) { _, _ in throw TestError() }) From 042013f1612175154e46e66192350ef46d3a046a Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Fri, 19 Mar 2021 12:34:28 +0000 Subject: [PATCH 42/45] Add an example for reductions(into:_:) --- Guides/Reductions.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Guides/Reductions.md b/Guides/Reductions.md index 87e6bf61..93b3cb68 100644 --- a/Guides/Reductions.md +++ b/Guides/Reductions.md @@ -8,17 +8,20 @@ Produces a sequence of values. This has the behaviour of reduce, but also returns all intermediate results. ```swift -let runningTotalExclusive = (1...5).reductions(0, +) -print(runningTotalExclusive) +let exclusiveRunningTotal = (1...5).reductions(0, +) +print(exclusiveRunningTotal) // prints [0, 1, 3, 6, 10, 15] -let runningTotalInclusive = (1...5).reductions(+) -print(runningTotalInclusive) -// prints [1, 3, 6, 10, 15] +var value = 0 +let intoRunningTotal = (1...5).reductions(into: &value, +=) +print(intoRunningTotal) +// prints [0, 1, 3, 6, 10, 15] +print(value) +// prints 15 -let runningMinimum = [3, 4, 2, 3, 1].reductions(min) -print(runningMinimum) -// prints [3, 3, 2, 2, 1] +let inclusiveRunningTotal = (1...5).reductions(+) +print(inclusiveRunningTotal) +// prints [1, 3, 6, 10, 15] ``` ## Detailed Design From 5f55e8bb732edcb1cde005744a10d825768d04da Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Sun, 21 Mar 2021 12:35:25 +0000 Subject: [PATCH 43/45] Improve the documentation of the return value --- Sources/Algorithms/Reductions.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 4a395320..e2d73b91 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -30,7 +30,8 @@ extension LazySequenceProtocol { /// - initial: The value to use as the initial value. /// - transform: A closure that combines the previously reduced result and /// the next element in the receiving sequence. - /// - Returns: A sequence of transformed elements. + /// - Returns: A sequence of the initial value followed by the reduced + /// elements. /// /// - Complexity: O(1) @inlinable @@ -62,7 +63,8 @@ extension LazySequenceProtocol { /// - initial: The value to use as the initial value. /// - transform: A closure that combines the previously reduced result and /// the next element in the receiving sequence. - /// - Returns: A sequence of transformed elements. + /// - Returns: A sequence of the initial value followed by the reduced + /// elements. /// /// - Complexity: O(1) @inlinable @@ -93,7 +95,7 @@ extension Sequence { /// - initial: The value to use as the initial value. /// - transform: A closure that combines the previously reduced result and /// the next element in the receiving sequence. - /// - Returns: An array of transformed elements. + /// - Returns: An array of the initial value followed by the reduced elements. /// /// - Complexity: O(n) @inlinable @@ -125,7 +127,7 @@ extension Sequence { /// - initial: The value to use as the initial value. /// - transform: A closure that combines the previously reduced result and /// the next element in the receiving sequence. - /// - Returns: An array of transformed elements. + /// - Returns: An array of the initial value followed by the reduced elements. /// /// - Complexity: O(n) @inlinable @@ -319,7 +321,7 @@ extension LazySequenceProtocol { /// - Parameters: /// - transform: A closure that combines the previously reduced result and /// the next element in the receiving sequence. - /// - Returns: An array of accumulated elements. + /// - Returns: A sequence of the reduced elements. /// /// - Complexity: O(1) @inlinable @@ -348,7 +350,7 @@ extension Sequence { /// - Parameters: /// - transform: A closure that combines the previously reduced result and /// the next element in the receiving sequence. - /// - Returns: An array of accumulated elements. + /// - Returns: An array of the reduced elements. /// /// - Complexity: O(n) @inlinable From d4daf2c5a8c3b867a9b1d7161beabebd0c044102 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Mon, 22 Mar 2021 13:55:50 +0000 Subject: [PATCH 44/45] Update complexity note --- Sources/Algorithms/Reductions.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index e2d73b91..31e561a1 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -97,7 +97,7 @@ extension Sequence { /// the next element in the receiving sequence. /// - Returns: An array of the initial value followed by the reduced elements. /// - /// - Complexity: O(n) + /// - Complexity: O(_n_), where _n_ is the length of the sequence. @inlinable public func reductions( _ initial: Result, @@ -129,7 +129,7 @@ extension Sequence { /// the next element in the receiving sequence. /// - Returns: An array of the initial value followed by the reduced elements. /// - /// - Complexity: O(n) + /// - Complexity: O(_n_), where _n_ is the length of the sequence. @inlinable public func reductions( into initial: inout Result, @@ -352,7 +352,7 @@ extension Sequence { /// the next element in the receiving sequence. /// - Returns: An array of the reduced elements. /// - /// - Complexity: O(n) + /// - Complexity: O(_n_), where _n_ is the length of the sequence. @inlinable public func reductions( _ transform: (Element, Element) throws -> Element From 6660d642cdc0093b19ca20cdb2845faa79b83767 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Mon, 22 Mar 2021 14:10:23 +0000 Subject: [PATCH 45/45] Use the reduce documentation for inspiration for the discussion of reductions --- Sources/Algorithms/Reductions.swift | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Sources/Algorithms/Reductions.swift b/Sources/Algorithms/Reductions.swift index 31e561a1..627cea48 100644 --- a/Sources/Algorithms/Reductions.swift +++ b/Sources/Algorithms/Reductions.swift @@ -91,6 +91,20 @@ extension Sequence { /// // prints [0, 1, 3, 6, 10] /// ``` /// + /// When `reductions(_:_:)` is called, the following steps occur: + /// + /// 1. The `initial` result is added to an array of results. + /// 2. The `transform` closure is called with the `initial` result and the + /// first element of the sequence, appending the result to the array. + /// 3. The closure is called again repeatedly with the updated accumulating + /// result and each element of the sequence, adding each result to the + /// array. + /// 4. When the sequence is exhausted, the results array is returned to the + /// caller. + /// + /// If the sequence has no elements, `transform` is never executed and + /// an array containing only the `initial` result is returned. + /// /// - Parameters: /// - initial: The value to use as the initial value. /// - transform: A closure that combines the previously reduced result and @@ -123,6 +137,20 @@ extension Sequence { /// // prints [0, 1, 3, 6, 10] /// ``` /// + /// When `reductions(into:_:_)` is called, the following steps occur: + /// + /// 1. The `initial` result is added to an array of results. + /// 2. The `transform` closure is called with the `initial` result and the + /// first element of the sequence, appending the result to the array. + /// 3. The closure is called again repeatedly with the updated accumulating + /// result and each element of the sequence, adding each result to the + /// array. + /// 4. When the sequence is exhausted, the results array is returned to the + /// caller. + /// + /// If the sequence has no elements, `transform` is never executed and + /// an array containing only the `initial` result is returned. + /// /// - Parameters: /// - initial: The value to use as the initial value. /// - transform: A closure that combines the previously reduced result and @@ -347,6 +375,22 @@ extension Sequence { /// // prints [1, 3, 6, 10] /// ``` /// + /// When `reductions(_:)` is called, the following steps occur: + /// + /// 1. The `transform` closure is called with the first and second elements + /// of the sequence, appending the result to an array of results. + /// 2. The closure is called again repeatedly with the updated accumulating + /// result and the next element of the sequence, adding each result to the + /// array. + /// 3. When the sequence is exhausted, the results array is returned to the + /// caller. + /// + /// If the sequence has no elements, `transform` is never executed and + /// an empty array is returned. + /// + /// If the sequence has one element, `transform` is never executed and + /// an array containing only that first element is returned. + /// /// - Parameters: /// - transform: A closure that combines the previously reduced result and /// the next element in the receiving sequence.