From 92e677b5426b19feab5ff1cbd1eea50f96f2f658 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Sun, 24 May 2026 05:00:47 -0700 Subject: [PATCH] Correct reductions docs: exclusive variants do not emit the initial value The doc comments for the exclusive reductions overloads stated that the returned sequence is "the initial value followed by the reduced elements". That does not match the behavior: the iterator pulls a base element before emitting anything, so the first emitted element is the result of combining the initial value with the first base element, and the initial value is never emitted. For an empty base sequence the result is empty rather than a single-element sequence containing the initial value. This is the behavior the existing tests already pin down, and changing it would be source-breaking, so this corrects the documentation to match. Updates all four exclusive overloads (throwing and non-throwing, value and into:) and adds empty-base tests covering the now-documented edge case. The inclusive reductions and the DocC Reductions guide already describe the actual behavior and are left unchanged. --- .../AsyncExclusiveReductionsSequence.swift | 22 +++++++++------ ...cThrowingExclusiveReductionsSequence.swift | 22 +++++++++------ .../AsyncAlgorithmsTests/TestReductions.swift | 28 +++++++++++++++++++ 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift index 582317c1..4cfba6bf 100644 --- a/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift @@ -14,15 +14,18 @@ extension AsyncSequence { /// Returns an asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous 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 an asynchronous sequence. + /// Starting from the initial value, this combines each element of the base + /// asynchronous sequence with the running result and emits each intermediate + /// result. The initial value itself is not emitted, so the first element of + /// the returned sequence is the result of combining the initial value with + /// the first element of the base sequence. If the base sequence is empty, the + /// returned sequence is also empty. /// /// - 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 asynchronous sequence, which it returns. - /// - Returns: An asynchronous sequence of the initial value followed by the reduced - /// elements. + /// - Returns: An asynchronous sequence of the reduced elements. @available(AsyncAlgorithms 1.0, *) @inlinable public func reductions( @@ -37,16 +40,19 @@ extension AsyncSequence { /// Returns an asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous 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 an asynchronous sequence. + /// Starting from the initial value, this combines each element of the base + /// asynchronous sequence with the running result and emits each intermediate + /// result. The initial value itself is not emitted, so the first element of + /// the returned sequence is the result of combining the initial value with + /// the first element of the base sequence. If the base sequence is empty, the + /// returned sequence is also empty. /// /// - 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 asynchronous sequence, mutating the /// previous result instead of returning a value. - /// - Returns: An asynchronous sequence of the initial value followed by the reduced - /// elements. + /// - Returns: An asynchronous sequence of the reduced elements. @available(AsyncAlgorithms 1.0, *) @inlinable public func reductions( diff --git a/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift index 35ea3ef5..14c2fc2c 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift @@ -14,16 +14,19 @@ extension AsyncSequence { /// Returns an asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous sequence using the given error-throwing closure. /// - /// This can be seen as applying the reduce function to each element and - /// providing the initial value followed by these results as an asynchronous sequence. + /// Starting from the initial value, this combines each element of the base + /// asynchronous sequence with the running result and emits each intermediate + /// result. The initial value itself is not emitted, so the first element of + /// the returned sequence is the result of combining the initial value with + /// the first element of the base sequence. If the base sequence is empty, the + /// returned sequence is also empty. /// /// - 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 asynchronous sequence and returns /// the result. If the closure throws an error, the sequence throws. - /// - Returns: An asynchronous sequence of the initial value followed by the reduced - /// elements. + /// - Returns: An asynchronous sequence of the reduced elements. @available(AsyncAlgorithms 1.0, *) @inlinable public func reductions( @@ -38,8 +41,12 @@ extension AsyncSequence { /// Returns an asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous sequence using the given error-throwing closure. /// - /// This can be seen as applying the reduce function to each element and - /// providing the initial value followed by these results as an asynchronous sequence. + /// Starting from the initial value, this combines each element of the base + /// asynchronous sequence with the running result and emits each intermediate + /// result. The initial value itself is not emitted, so the first element of + /// the returned sequence is the result of combining the initial value with + /// the first element of the base sequence. If the base sequence is empty, the + /// returned sequence is also empty. /// /// - Parameters: /// - initial: The value to use as the initial value. @@ -47,8 +54,7 @@ extension AsyncSequence { /// the next element in the receiving asynchronous sequence, mutating the /// previous result instead of returning a value. If the closure throws an /// error, the sequence throws. - /// - Returns: An asynchronous sequence of the initial value followed by the reduced - /// elements. + /// - Returns: An asynchronous sequence of the reduced elements. @available(AsyncAlgorithms 1.0, *) @inlinable public func reductions( diff --git a/Tests/AsyncAlgorithmsTests/TestReductions.swift b/Tests/AsyncAlgorithmsTests/TestReductions.swift index ba4366ea..f460c3e4 100644 --- a/Tests/AsyncAlgorithmsTests/TestReductions.swift +++ b/Tests/AsyncAlgorithmsTests/TestReductions.swift @@ -27,6 +27,34 @@ final class TestReductions: XCTestCase { XCTAssertNil(pastEnd) } + func test_reductions_empty() async { + let sequence = [Int]().async.reductions("") { partial, value in + partial + "\(value)" + } + var iterator = sequence.makeAsyncIterator() + var collected = [String]() + while let item = await iterator.next() { + collected.append(item) + } + XCTAssertEqual(collected, []) + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + } + + func test_throwing_reductions_empty() async throws { + let sequence = [Int]().async.reductions("") { (partial, value) throws -> String in + partial + "\(value)" + } + var iterator = sequence.makeAsyncIterator() + var collected = [String]() + while let item = try await iterator.next() { + collected.append(item) + } + XCTAssertEqual(collected, []) + let pastEnd = try await iterator.next() + XCTAssertNil(pastEnd) + } + func test_inclusive_reductions() async { let sequence = [1, 2, 3, 4].async.reductions { $0 + $1 } var iterator = sequence.makeAsyncIterator()