Skip to content

In-place scan and un-scan #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
80 changes: 80 additions & 0 deletions Guides/Accumulate.md
Original file line number Diff line number Diff line change
@@ -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<Double>()
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 `<numeric>`
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.)

<!-- Link references for other languages -->

[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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
78 changes: 78 additions & 0 deletions Sources/Algorithms/Accumulate.swift
Original file line number Diff line number Diff line change
@@ -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: &current)
}
}

/// 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: &currentIndex)
}
}
}
56 changes: 56 additions & 0 deletions Tests/SwiftAlgorithmsTests/AccumulateTests.swift
Original file line number Diff line number Diff line change
@@ -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<Double>()
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))
}
}