From 7d5e048fc7c59ce8904bc28496b6e1ab05f61eb7 Mon Sep 17 00:00:00 2001 From: Daryle Walker Date: Thu, 26 Nov 2020 01:49:34 -0500 Subject: [PATCH] Add methods for in-place scan and un-scan Add a method to element-mutable collections that takes a closure that can fuse two element values and applies it to the collection, i.e. progressive reduce, a.k.a. scan. Add another method that takes a closure that can separate an element value from another and applies it to a collection, doing the inverse of the first method. --- CHANGELOG.md | 9 ++- Guides/Accumulate.md | 80 +++++++++++++++++++ README.md | 1 + Sources/Algorithms/Accumulate.swift | 78 ++++++++++++++++++ .../AccumulateTests.swift | 56 +++++++++++++ 5 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 Guides/Accumulate.md create mode 100644 Sources/Algorithms/Accumulate.swift create mode 100644 Tests/SwiftAlgorithmsTests/AccumulateTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 84f0a457..f8c63e99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,14 @@ package updates, you can specify your package dependency using ## [Unreleased] -*No changes yet.* +### Additions + +- Two methods have been added to element-mutable collections. The + `accumulate(via:)` method does a scan (a.k.a. progressive reduce) with a + given combining closure, but assigns the results on top of the existing + elements instead of returning a separate sequence. The `disperse(via:)` + method does the counter-operation, assuming it's given the appropriate + counter-closure. --- diff --git a/Guides/Accumulate.md b/Guides/Accumulate.md new file mode 100644 index 00000000..cfd80c21 --- /dev/null +++ b/Guides/Accumulate.md @@ -0,0 +1,80 @@ +# Accumulate + +[[Source](../Sources/Algorithms/Accumulate.swift) | + [Tests](../Tests/SwiftAlgorithmsTests/AccumulateTests.swift)] + +Perform `scan(_:)_:)` on a given collection with a given operation, but +overwrite the collection with the results instead of using a separate returned +instance. + +```swift +var numbers = Array(1...5) +print(numbers) // "[1, 2, 3, 4, 5]" +numbers.accumulate(via: +) +print(numbers) // "[1, 3, 6, 10, 15]" +numbers.disperse(via: -) +print(numbers) // "[1, 2, 3, 4, 5]" + +var empty = Array() +print(empty) // "[]" +empty.accumulate(via: *) +print(empty) // "[]" +empty.disperse(via: /) +print(empty) // "[]" +``` + +`accumulate(via:)` takes a closure that fuses the values of two elements. +`disperse(via:)` takes a closure that returns its first argument after the +contribution of the second argument has been removed from it. + +## Detailed Design + +New methods are added to collections that can do per-element mutations: + +```swift +extension MutableCollection { + mutating func accumulate( + via combine: (Element, Element) throws -> Element + ) rethrows + + mutating func disperse( + via sever: (Element, Element) throws -> Element + ) rethrows +} +``` + +Both methods apply their given closures to adjacent pairs of elements, starting +from the first and second elements to the next-to-last and last elements. The +order the elements are submitted to the closures differ; `combine` takes the +earlier element first and the latter second, while that's reversed for `sever`. + +### Complexity + +Calling these methods is O(_n_), where _n_ is the length of the collection. + +### Naming + +The name for `accumulate` was chosen from the similar action category that the +C++ standard library function with the same name. The name for `disperse` was +taken from a list of antonyms for the first method's name. Suggestions for +better names would be appreciated. + +### Comparison with other languages + +**C++:** Has a [`partial_sum`][C++Partial] function from the `` +library which takes a bounding input iterator pair, an output iterator and a +combining function (defaulting to `+` if not given), and writes into the output +iterator the progressive combination of all the input values read so far. The +[`inclusive_scan`][C++Inclusive] function from the same library works the same +way. That library finally has [`exclusive_scan`][C++Exclusive] which has an +extra parameter for an initial seed value, writing to the output iterator the +progressive combination of the seed value and all the input values prior to the +last-read one. (The library also has a function named +[`accumulate`][C++Accumulate], but it acts like Swift's `reduce(_:_:)` method.) + + + +[C++Partial]: https://en.cppreference.com/w/cpp/algorithm/partial_sum +[C++Inclusive]: https://en.cppreference.com/w/cpp/algorithm/inclusive_scan +[C++Exclusive]: https://en.cppreference.com/w/cpp/algorithm/exclusive_scan +[C++Accumulate]: https://en.cppreference.com/w/cpp/algorithm/accumulate diff --git a/README.md b/README.md index 9033c90a..005e945b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Read more about the package, and the intent behind it, in the [announcement on s - [`rotate(toStartAt:)`, `rotate(subrange:toStartAt:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Rotate.md): In-place rotation of elements. - [`stablePartition(by:)`, `stablePartition(subrange:by:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Partition.md): A partition that preserves the relative order of the resulting prefix and suffix. +- [`accumulate(via:)`, `disperse(via:)`](./Guides/Accumulate.md): In-place scan (a.k.a. progressive reduce) and un-scan. #### Combining collections diff --git a/Sources/Algorithms/Accumulate.swift b/Sources/Algorithms/Accumulate.swift new file mode 100644 index 00000000..03af6db6 --- /dev/null +++ b/Sources/Algorithms/Accumulate.swift @@ -0,0 +1,78 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// accumulate(via:), disperse(via:) +//===----------------------------------------------------------------------===// + +extension MutableCollection { + /// Progressively replaces each element with the combination of its + /// (post-mutation) predecessor and itself, using the given closure to + /// generate the new values. + /// + /// For each pair of adjacent elements, the former is fed as the first + /// argument to the closure and the latter is fed as the second. Iteration + /// goes from the second element to the last. + /// + /// - Parameters: + /// - combine: The closure that fuses two values to a new one. + /// - Postcondition: `dropFirst()` is replaced by + /// `dropFirst().scan(first!, combine)`. There is no effect if the + /// collection has fewer than two elements. + /// + /// - Complexity: O(*n*), where *n* is the length of the collection. + public mutating func accumulate( + via combine: (Element, Element) throws -> Element + ) rethrows { + let end = endIndex + var previous = startIndex + guard previous < end else { return } + + var current = index(after: previous) + while current < end { + self[current] = try combine(self[previous], self[current]) + previous = current + formIndex(after: ¤t) + } + } + + /// Progressively replaces each element with the disassociation between its + /// (pre-mutation) predecessor and itself, using the given closure to generate + /// the new values. + /// + /// For each pair of adjacent elements, the former is fed as the second + /// argument to the closure and the latter is fed as the first. Iteration + /// goes from the second element to the last. + /// + /// - Parameters: + /// - sever: The closure that defuses a value out of another. + /// - Postcondition: Define `combine` as the counter-operation to `sever`, + /// such that `combine(sever(c, b), b)` is equivalent to `c`. Then calling + /// `accumulate(via: combine)` after running this method will set `self` to + /// a state equivalent to what it was before running this method. There is + /// no effect if the collection has fewer than two elements. + /// + /// - Complexity: O(*n*), where *n* is the length of the collection. + public mutating func disperse( + via sever: (Element, Element) throws -> Element + ) rethrows { + guard var previousValue = first else { return } + + let end = endIndex + var currentIndex = index(after: startIndex) + while currentIndex < end { + let currentValue = self[currentIndex] + self[currentIndex] = try sever(currentValue, previousValue) + previousValue = currentValue + formIndex(after: ¤tIndex) + } + } +} diff --git a/Tests/SwiftAlgorithmsTests/AccumulateTests.swift b/Tests/SwiftAlgorithmsTests/AccumulateTests.swift new file mode 100644 index 00000000..664c17a6 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/AccumulateTests.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +/// Unit tests for the `accumulate(via:)` and `disperse(via:)` methods. +final class AccumulateTests: XCTestCase { + /// Check that nothing happens with empty collections. + func testEmpty() { + var empty = EmptyCollection() + XCTAssertEqualSequences(empty, []) + empty.accumulate(via: +) + XCTAssertEqualSequences(empty, []) + empty.disperse(via: -) + XCTAssertEqualSequences(empty, []) + } + + /// Check that nothing happens with one-element collections. + func testSingle() { + var single = CollectionOfOne(1.1) + XCTAssertEqualSequences(single, [1.1]) + single.accumulate(via: +) + XCTAssertEqualSequences(single, [1.1]) + single.disperse(via: -) + XCTAssertEqualSequences(single, [1.1]) + } + + /// Check a two-element collection. + func testDouble() { + var sample = [5, 2] + XCTAssertEqualSequences(sample, [5, 2]) + sample.accumulate(via: *) + XCTAssertEqualSequences(sample, [5, 10]) + sample.disperse(via: /) + XCTAssertEqualSequences(sample, [5, 2]) + } + + /// Check a long collection. + func testLong() { + var sample1 = Array(repeating: 1, count: 5) + XCTAssertEqualSequences(sample1, repeatElement(1, count: 5)) + sample1.accumulate(via: +) + XCTAssertEqualSequences(sample1, 1...5) + sample1.disperse(via: -) + XCTAssertEqualSequences(sample1, repeatElement(1, count: 5)) + } +}