Swift Async Algorithms

repository·main·Indexed 25 days ago

https://github.com/apple/swift-async-algorithms

An open-source collection of asynchronous sequence algorithms and types for Swift's async/await concurrency model. It provides tools for handling values over time (debounce, throttle), combining sequences (combineLatest, merge, zip, chain), and creating asynchronous sequences (AsyncChannel, .async). The package includes specialized types like AsyncBufferedByteIterator for high-performance byte streaming and supports cross-platform use on macOS (Xcode 14+) and Linux.

Tokens
27K
Snippets
66
Records
134
Agent score
86%

What's inside swift-async-algorithms

  1. Overview of Swift Async Algorithms

    main

    Swift Async Algorithms is an open-source package providing asynchronous sequence and advanced algorithms that involve concurrency. It focuses on:

    • First-class integration with async/await.
    • Time-based algorithms (e.g., debounce, throttle).
    • Order-based algorithms (e.g., combineLatest, merge).
    • Cross-platform support.
  2. Explore Swift Async Algorithms topics

    main

    The package provides a variety of asynchronous sequence algorithms and related types. Key topics include:

    • Sequence Manipulation: AdjacentPairs, Chain, Chunked, Compacted, Intersperse, Joined, RemoveDuplicates, Zip.
    • Concurrency & Timing: Debounce, Throttle, Timer, Merge, CombineLatest.
    • Buffering & Flow Control: BufferedBytes, Channel, Share.
    • State & Accumulation: Reductions, Collections.
    • Advanced Patterns: Effects, Lazy.
  3. Understand the Generalized Asynchronous Streaming Protocols

    main

    The proposed streaming model introduces four protocols to handle asynchronous data flow, addressing limitations in AsyncSequence such as per-element overhead for small types (like bytes), lack of support for noncopyable types, and the inability to express push-based writing within structured concurrency.

    The protocols are categorized by who manages the buffer ownership (Caller vs. Callee) and the direction of data flow (Read vs. Write):

    ProtocolBuffer OwnerDirectionBest Use Case
    AsyncReaderCalleeReadData from external sources (e.g., IPC, kernel-managed buffers) to allow zero-copy transfer.
    CallerAsyncReaderCallerReadReading into an existing allocation or resource-constrained environments (e.g., Embedded Swift).
    CallerAsyncWriterCallerWriteData already exists in a buffer (e.g., writing an Array to disk).
    AsyncWriterCalleeWriteDestination already has storage (e.g., pre-registered I/O buffers) to allow in-place writing.
  4. Use MultiProducerSingleConsumerAsyncChannel for multi-producer systems

    main
    The MultiProducerSingleConsumerAsyncChannel is a root asynchronous primitive designed for modeling asynchronous multi-producer-single-consumer systems. Unlike AsyncStream or AsyncChannel, it provides strict multi-producer-single-consumer guarantees and supports configurable backpressure strategies for both synchronous and asynchronous producers.
  5. Combine asynchronous sequences using zip(_:_:)

    main

    The zip(_:_:) function combines the latest values produced from two or more asynchronous sequences into a single asynchronous sequence of tuples.

    Each iteration of the resulting sequence awaits values from all base sequences concurrently. The iteration produces a tuple containing one element from each base sequence.

    Termination and Error Behavior:

    • Termination: If any of the base sequences terminates (returns nil), the zipped sequence immediately terminates and returns nil. All other concurrent iterations are cancelled.
    • Errors: If any base sequence throws an error, the error is rethrown by the zipped sequence, and all other concurrent iterations are cancelled.
    • Concurrency: Because iterations happen concurrently, all base sequences, their elements, and their iterators must conform to Sendable.
    let appleFeed = URL(string: "http://www.example.com/ticker?symbol=AAPL")!.lines
    let nasdaqFeed = URL(string: "http://www.example.com/ticker?symbol=^IXIC")!.lines
    
    for try await (apple, nasdaq) in zip(appleFeed, nasdaqFeed) {
      print("APPL: \(apple) NASDAQ: \(nasdaq)")
    }
  6. Use flatMapLatest to switch to the most recent asynchronous sequence

    main

    The flatMapLatest operator transforms elements from an asynchronous sequence into new asynchronous sequences, but it only emits elements from the most recent inner sequence. When a new element arrives from the base sequence, any ongoing iteration on the previous inner sequence is immediately cancelled, and iteration begins on the new sequence.

    This is ideal for scenarios where only the latest data matters, such as search-as-you-type, location updates, or dynamic configuration changes.

    let searchQuery = AsyncStream<String> { continuation in
      // User types into search field
      continuation.yield("swi")
      try? await Task.sleep(for: .milliseconds(100))
      continuation.yield("swift")
      try? await Task.sleep(for: .milliseconds(100))
      continuation.yield("swift async")
      continuation.finish()
    }
    
    let searchResults = searchQuery.flatMapLatest { query in
      performSearch(query) // Returns AsyncSequence<SearchResult>
    }
    
    for try await result in searchResults {
      print(result) // Only shows results from "swift async"
    }
  7. Handle upstream producer termination via setOnTerminationCallback

    main

    Producers can be notified when the channel terminates (e.g., due to task cancellation or the channel being deinitialized) by using source.setOnTerminationCallback.

    Termination of the producer occurs when:

    • The task consuming the channel is cancelled.
    • The channel itself is deinitialized.
    • The source is finished and all elements are consumed.
    • All sources (including additional sources) are deinitialized and all elements are consumed.
    let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel(
        of: Int.self,
        backpressureStrategy: .watermark(low: 2, high: 4)
    )
    var channel = consume channelAndSource.channel
    var source = consume channelAndSource.source
    source.setOnTerminationCallback { print("Terminated") }
    
    let task = Task {
        await channel.next()
    }
    task.cancel() // Prints "Terminated"
  8. Validate AsyncSequences using a Domain Specific Language

    main

    The validate function allows you to test AsyncSequence implementations using a visual, time-based domain-specific language (DSL). You can define inputs as strings where characters represent values or control events, specify the transformation logic, and define the expected output string. This approach provides deterministic testing for asynchronous sequences by explicitly defining the ordering of time.

    Key features:

    • Time progression: Use - to advance time.
    • Termination: Use | to represent the sequence returning nil.
    • Multiple Inputs: Result builders allow testing operators like merge by providing multiple input specifications.
    • Visual Alignment: Spaces are ignored in terms of time advancement; they are used for visual alignment and do not represent events.
    validate {
      "a--b--c---|"
      $0.inputs[0].map { $0.capitalized }
      "A--B--C---|"
    }
  9. Compact an AsyncSequence using compacted()

    main

    If you have an AsyncSequence containing optional values, you can use the compacted() method to filter out nil values and return a sequence of unwrapped elements. This is a more efficient alternative to .compactMap { $0 } because it avoids the overhead of executing and storing a closure.

    Behaviorally, compacted() is equivalent to .compactMap { $0 }. The resulting sequence will throw if the base sequence throws, and will not throw if the base sequence does not throw.

  10. Intersperse values in an AsyncSequence using `interspersed(with:)`

    main

    Use the interspersed(with:) method to insert a separator value between every element of an asynchronous sequence. The separator is only placed between elements; it is not added before the first element or after the last element.

    • If the base sequence is empty, the resulting sequence is also empty.
    • If the base sequence throws during iteration, AsyncInterspersedSequence will also throw.
    • AsyncInterspersedSequence is conditionally Sendable if both the base sequence and the element type are Sendable.
    let numbers = [1, 2, 3].async.interspersed(with: 0)
    for await number in numbers {
      print(number)
    }
    // prints 1 0 2 0 3
    
    let empty = [].async.interspersed(with: 0)
    // await Array(empty) == []