Skip to content

async chunked sequences can produce empty chunks #312

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 3 commits 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
96 changes: 96 additions & 0 deletions Evolution/0012-produce-empty-chunks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Produce Empty Chunks

* Proposal: [0012](0012-produce-empty-chunks.md)
* Author: [Rick Newton-Rogers](https://github.com/rnro)
* Review Manager: TBD
* Status: **Implemented**

* Implementation:
[Source](https://github.com/rnewtonrogers/swift-async-algorithms/blob/allow_empty_chunks/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift) |
[Tests](https://github.com/rnewtonrogers/swift-async-algorithms/blob/allow_empty_chunks/Tests/AsyncAlgorithmsTests/TestChunk.swift)

## Introduction

At the moment it is possible to use a signal `AsyncSequence` to provide marks at which elements of a primary
`AsyncSequence` should be 'chunked' into collections. However if one or more signals arrive when there are no elements
from the primary sequence to vend as output then they will be ignored.

## Motivation

As noted in [a GitHub Issue](https://github.com/apple/swift-async-algorithms/issues/247) it could be useful to output empty
chunks in the outlined case to provide information of a lack of activity on the primary sequence. This would likely be
particularly useful when combined with a timer as a signaling source.

## Proposed solution

Modify the API of the `AsyncSequence` `chunks` and `chunked` extensions to allow specifying of a new parameter
(`produceEmptyChunks`) which determines if the output sequence produces empty chunks. The new parameter will retain the
previous behavior by default.

## Detailed design

The modified API will look as follows:
```swift
extension AsyncSequence {
/// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when a signal `AsyncSequence` produces an element.
public func chunks<Signal, Collected: RangeReplaceableCollection>(ofCount count: Int, or signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, Signal> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: count, signal: signal, produceEmptyChunks: produceEmptyChunks)
}

/// Creates an asynchronous sequence that creates chunks of a given count or when a signal `AsyncSequence` produces an element.
public func chunks<Signal>(ofCount count: Int, or signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], Signal> {
chunks(ofCount: count, or: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks)
}

/// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when a signal `AsyncSequence` produces an element.
public func chunked<Signal, Collected: RangeReplaceableCollection>(by signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, Signal> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: signal, produceEmptyChunks: produceEmptyChunks)
}

/// Creates an asynchronous sequence that creates chunks when a signal `AsyncSequence` produces an element.
public func chunked<Signal>(by signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], Signal> {
chunked(by: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks)
}

/// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when an `AsyncTimerSequence` fires.
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func chunks<C: Clock, Collected: RangeReplaceableCollection>(ofCount count: Int, or timer: AsyncTimerSequence<C>, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, AsyncTimerSequence<C>> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: count, signal: timer, produceEmptyChunks: produceEmptyChunks)
}

/// Creates an asynchronous sequence that creates chunks of a given count or when an `AsyncTimerSequence` fires.
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func chunks<C: Clock>(ofCount count: Int, or timer: AsyncTimerSequence<C>, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], AsyncTimerSequence<C>> {
chunks(ofCount: count, or: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks)
}

/// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when an `AsyncTimerSequence` fires.
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func chunked<C: Clock, Collected: RangeReplaceableCollection>(by timer: AsyncTimerSequence<C>, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, AsyncTimerSequence<C>> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: timer, produceEmptyChunks: produceEmptyChunks)
}

/// Creates an asynchronous sequence that creates chunks when an `AsyncTimerSequence` fires.
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func chunked<C: Clock>(by timer: AsyncTimerSequence<C>, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], AsyncTimerSequence<C>> {
chunked(by: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks)
}
}
```
The previous API will be marked as deprecated and `@_disfavoredOverload` to avoid ambiguity with the new versions.


## Effect on API resilience

This change is API-safe due to the default value but ABI-unsafe.

## Alternatives considered

- Providing a config struct to future-proof the API against further changes in the future was rejected because it
seems to add overhead defending against an unlikely event.
- Providing entirely new API without deprecating the old one was ruled out to avoid an explosion in API complexity
which increases maintenance burden and reduces readability of code.

## Acknowledgments

- [@tachyonics](https://github.com/tachyonics) for the initial GitHub issue describing the requirement.
45 changes: 26 additions & 19 deletions Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,47 @@

extension AsyncSequence {
/// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when a signal `AsyncSequence` produces an element.
public func chunks<Signal, Collected: RangeReplaceableCollection>(ofCount count: Int, or signal: Signal, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, Signal> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: count, signal: signal)
public func chunks<Signal, Collected: RangeReplaceableCollection>(ofCount count: Int, or signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, Signal> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: count, signal: signal, produceEmptyChunks: produceEmptyChunks)
}

/// Creates an asynchronous sequence that creates chunks of a given count or when a signal `AsyncSequence` produces an element.
public func chunks<Signal>(ofCount count: Int, or signal: Signal) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], Signal> {
chunks(ofCount: count, or: signal, into: [Element].self)
public func chunks<Signal>(ofCount count: Int, or signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], Signal> {
chunks(ofCount: count, or: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks)
}

/// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when a signal `AsyncSequence` produces an element.
public func chunked<Signal, Collected: RangeReplaceableCollection>(by signal: Signal, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, Signal> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: signal)
public func chunked<Signal, Collected: RangeReplaceableCollection>(by signal: Signal, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, Signal> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: signal, produceEmptyChunks: produceEmptyChunks)
}

/// Creates an asynchronous sequence that creates chunks when a signal `AsyncSequence` produces an element.
public func chunked<Signal>(by signal: Signal) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], Signal> {
chunked(by: signal, into: [Element].self)
public func chunked<Signal>(by signal: Signal, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], Signal> {
chunked(by: signal, into: [Element].self, produceEmptyChunks: produceEmptyChunks)
}

/// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when an `AsyncTimerSequence` fires.
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func chunks<C: Clock, Collected: RangeReplaceableCollection>(ofCount count: Int, or timer: AsyncTimerSequence<C>, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, AsyncTimerSequence<C>> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: count, signal: timer)
public func chunks<C: Clock, Collected: RangeReplaceableCollection>(ofCount count: Int, or timer: AsyncTimerSequence<C>, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, AsyncTimerSequence<C>> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: count, signal: timer, produceEmptyChunks: produceEmptyChunks)
}

/// Creates an asynchronous sequence that creates chunks of a given count or when an `AsyncTimerSequence` fires.
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func chunks<C: Clock>(ofCount count: Int, or timer: AsyncTimerSequence<C>) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], AsyncTimerSequence<C>> {
chunks(ofCount: count, or: timer, into: [Element].self)
public func chunks<C: Clock>(ofCount count: Int, or timer: AsyncTimerSequence<C>, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], AsyncTimerSequence<C>> {
chunks(ofCount: count, or: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks)
}

/// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when an `AsyncTimerSequence` fires.
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func chunked<C: Clock, Collected: RangeReplaceableCollection>(by timer: AsyncTimerSequence<C>, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, AsyncTimerSequence<C>> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: timer)
public func chunked<C: Clock, Collected: RangeReplaceableCollection>(by timer: AsyncTimerSequence<C>, into: Collected.Type, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, AsyncTimerSequence<C>> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: timer, produceEmptyChunks: produceEmptyChunks)
}

/// Creates an asynchronous sequence that creates chunks when an `AsyncTimerSequence` fires.
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func chunked<C: Clock>(by timer: AsyncTimerSequence<C>) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], AsyncTimerSequence<C>> {
chunked(by: timer, into: [Element].self)
public func chunked<C: Clock>(by timer: AsyncTimerSequence<C>, produceEmptyChunks: Bool = false) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], AsyncTimerSequence<C>> {
chunked(by: timer, into: [Element].self, produceEmptyChunks: produceEmptyChunks)
}
}

Expand All @@ -74,12 +74,14 @@ public struct AsyncChunksOfCountOrSignalSequence<Base: AsyncSequence, Collected:
typealias Merged = AsyncMerge2Sequence<ChainedBase, EitherMappedSignal>

let count: Int?
let produceEmptyChunks: Bool
var iterator: Merged.AsyncIterator
var terminated = false

init(iterator: Merged.AsyncIterator, count: Int?) {
init(iterator: Merged.AsyncIterator, count: Int?, produceEmptyChunks: Bool) {
self.count = count
self.iterator = iterator
self.produceEmptyChunks = produceEmptyChunks
}

public mutating func next() async rethrows -> Collected? {
Expand All @@ -104,6 +106,9 @@ public struct AsyncChunksOfCountOrSignalSequence<Base: AsyncSequence, Collected:
if result != nil {
return result
}
if self.produceEmptyChunks {
return Collected()
}
}
}
return result
Expand All @@ -113,19 +118,21 @@ public struct AsyncChunksOfCountOrSignalSequence<Base: AsyncSequence, Collected:
let base: Base
let signal: Signal
let count: Int?
init(_ base: Base, count: Int?, signal: Signal) {
let produceEmptyChunks: Bool
init(_ base: Base, count: Int?, signal: Signal, produceEmptyChunks: Bool) {
if let count = count {
precondition(count > 0)
}

self.base = base
self.count = count
self.signal = signal
self.produceEmptyChunks = produceEmptyChunks
}

public func makeAsyncIterator() -> Iterator {

return Iterator(iterator: merge(chain(base.map { Either.element($0) }, [.terminal].async), signal.map { _ in Either.signal }).makeAsyncIterator(), count: count)
return Iterator(iterator: merge(chain(base.map { Either.element($0) }, [.terminal].async), signal.map { _ in Either.signal }).makeAsyncIterator(), count: count, produceEmptyChunks: produceEmptyChunks)
}
}

Expand Down
63 changes: 63 additions & 0 deletions Sources/AsyncAlgorithms/Deprecated.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@


extension AsyncSequence {
/// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when a signal `AsyncSequence` produces an element.
@_disfavoredOverload
@available(*, deprecated, renamed: "chunks(ofCount:or:into:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.")
public func chunks<Signal, Collected: RangeReplaceableCollection>(ofCount count: Int, or signal: Signal, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, Signal> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: count, signal: signal, produceEmptyChunks: false)
}

/// Creates an asynchronous sequence that creates chunks of a given count or when a signal `AsyncSequence` produces an element.
@_disfavoredOverload
@available(*, deprecated, renamed: "chunks(ofCount:or:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.")
public func chunks<Signal>(ofCount count: Int, or signal: Signal) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], Signal> {
chunks(ofCount: count, or: signal, into: [Element].self)
}

/// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when a signal `AsyncSequence` produces an element.
@_disfavoredOverload
@available(*, deprecated, renamed: "chunked(by:into:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.")
public func chunked<Signal, Collected: RangeReplaceableCollection>(by signal: Signal, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, Signal> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: signal, produceEmptyChunks: false)
}

/// Creates an asynchronous sequence that creates chunks when a signal `AsyncSequence` produces an element.
@_disfavoredOverload
@available(*, deprecated, renamed: "chunked(by:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.")
public func chunked<Signal>(by signal: Signal) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], Signal> {
chunked(by: signal, into: [Element].self)
}

/// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when an `AsyncTimerSequence` fires.
@_disfavoredOverload
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
@available(*, deprecated, renamed: "chunks(ofCount:or:into:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.")
public func chunks<C: Clock, Collected: RangeReplaceableCollection>(ofCount count: Int, or timer: AsyncTimerSequence<C>, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, AsyncTimerSequence<C>> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: count, signal: timer, produceEmptyChunks: false)
}

/// Creates an asynchronous sequence that creates chunks of a given count or when an `AsyncTimerSequence` fires.
@_disfavoredOverload
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
@available(*, deprecated, renamed: "chunks(ofCount:or:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.")
public func chunks<C: Clock>(ofCount count: Int, or timer: AsyncTimerSequence<C>) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], AsyncTimerSequence<C>> {
chunks(ofCount: count, or: timer, into: [Element].self)
}

/// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when an `AsyncTimerSequence` fires.
@_disfavoredOverload
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
@available(*, deprecated, renamed: "chunked(by:into:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.")
public func chunked<C: Clock, Collected: RangeReplaceableCollection>(by timer: AsyncTimerSequence<C>, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence<Self, Collected, AsyncTimerSequence<C>> where Collected.Element == Element {
AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: timer, produceEmptyChunks: false)
}

/// Creates an asynchronous sequence that creates chunks when an `AsyncTimerSequence` fires.
@_disfavoredOverload
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
@available(*, deprecated, renamed: "chunked(by:produceEmptyChunks:)", message: "This method has been deprecated to allow the option for sequences to produce empty chunks.")
public func chunked<C: Clock>(by timer: AsyncTimerSequence<C>) -> AsyncChunksOfCountOrSignalSequence<Self, [Element], AsyncTimerSequence<C>> {
chunked(by: timer, into: [Element].self)
}
}
Loading