Skip to content
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

Deferred algorithm #222

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open

Deferred algorithm #222

wants to merge 6 commits into from

Conversation

tcldr
Copy link

@tcldr tcldr commented Oct 20, 2022

Deferred

Introduction

AsyncDeferredSequence provides a convenient way to postpone the initialization of a sequence to the point where it is requested by a sequence consumer.

Motivation

Some source sequences may perform expensive work on initialization. This could be network activity, sensor activity, or anything else that consumes system resources. While this can be mitigated in some simple situtations by only passing around a sequence at the point of use, often it is favorable to be able to pass a sequence to its eventual point of use without commencing its initialization process. This is especially true for sequences which are intended for multicast/broadcast for which a reliable startup and shutdown procedure is essential.

A simple example of a seqeunce which may benefit from being deferred is provided in the documentation for AsyncStream:

extension QuakeMonitor {

    static var quakes: AsyncStream<Quake> {
        AsyncStream { continuation in
            let monitor = QuakeMonitor()
            monitor.quakeHandler = { quake in
                continuation.yield(quake)
            }
            continuation.onTermination = { @Sendable _ in
                 monitor.stopMonitoring()
            }
            monitor.startMonitoring()
        }
    }
}

In the supplied code sample, the closure provided to the AsyncStream initializer will be executed immediately upon initialization; QuakeMonitor.startMonitoring() will be called, and the stream will then begin buffering its contents waiting to be iterated. Whilst this behavior is sometimes desirable, on other occasions it can cause system resources to be consumed unnecessarily.

let nonDeferredSequence = QuakeMonitor.quakes //  `Quake.startMonitoring()` is called now!

...
// at some arbitrary point, possibly hours later...
for await quake in nonDeferredSequence {
    print("Quake: \(quake.date)")
}
// Prints out hours of previously buffered quake data before showing the latest

Proposed solution

AsyncDeferredSequence provides a way to postpone the initialization of an an arbitrary async sequence until the point of use:

let deferredSequence = deferred(QuakeMonitor.quakes) // Now, initialization is postponed

...
// at some arbitrary point, possibly hours later...
for await quake in deferredSequence {  //  `Quake.startMonitoring()` is now called
    print("Quake: \(quake.date)")
}
// Prints out only the latest quake data

Now, potentially expensive system resources are consumed only at the point they're needed.

Detailed design

AsyncDeferredSequence is a trivial algorithm supported by some convenience functions.

Functions

public func deferred<Base: AsyncSequence>(_ createSequence: @escaping @Sendable () async -> Base) -> AsyncDeferredSequence<Base>
public func deferred<Base: AsyncSequence>(_ createSequence: @autoclosure @escaping @Sendable () -> Base) -> AsyncDeferredSequence<Base>

The synchronous function can be auto-escaped, simplifying the call-site. While the async variant allows a sequence to be initialized within a concurrency context other than that of the end consumer.

public struct AsyncDeferredSequence<Base: AsyncSequence>: Sendable {
  public typealias Element = Base.Element
  public struct Iterator: AsyncIteratorProtocol {
    public mutating func next() async rethrows -> Element?
  }
  public func makeAsyncIterator() -> Iterator
}

Naming

The deferred(_:) function takes its inspiration from the Combine publisher of the same name with similar functionality. However, lazy(_:) could be quite fitting, too.

Comparison with other libraries

ReactiveX ReactiveX has an API definition of Defer as a top level operator for generating observables.

Combine Combine has an API definition of Deferred as a top-level convenience publisher.

Effect on API resilience

Deferred has a trivial implementation and is marked as @frozen and @inlinable. This removes the ability of this type and functions to be ABI resilient boundaries at the benefit of being highly optimizable.

Alternatives considered

@phausler phausler added the v1.1 Post-1.0 work label Oct 20, 2022
@phausler
Copy link
Member

Fantastic! I think this is perhaps one of the first few we should run as soon as we hit 1.0.

@tcldr
Copy link
Author

tcldr commented Oct 20, 2022

Great – thanks for the feedback! Should be incorporated now.

Evolution/NNNN-deferred.md Outdated Show resolved Hide resolved
Sources/AsyncAlgorithms/AsyncDeferredSequence.swift Outdated Show resolved Hide resolved
Sources/AsyncAlgorithms/AsyncDeferredSequence.swift Outdated Show resolved Hide resolved
@tcldr
Copy link
Author

tcldr commented Oct 24, 2022

@FranzBusch yes, much clearer. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
v1.1 Post-1.0 work
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants