├── Sources └── AsyncOperations │ ├── numberOfConcurrentTasks.swift │ ├── Index.swift │ ├── Sequence │ ├── Sequence+AsyncReduce.swift │ ├── Sequence+AsyncForEach.swift │ ├── Sequence+AsyncMap.swift │ ├── Sequence+AsyncFlatMap.swift │ ├── Sequence+AsyncCompactMap.swift │ ├── InternalForEach.swift │ ├── Sequence+AsyncFilter.swift │ ├── Sequence+AsyncAllSatisfy.swift │ ├── Sequence+AsyncContains.swift │ └── Sequence+AsyncFirst.swift │ ├── AsyncSequence │ └── AsyncSequence+AsyncForEach.swift │ ├── withOrderedTaskGroup.swift │ ├── Documentation.docc │ └── Documentation.md │ └── withThrowingOrderedTaskGroup.swift ├── Tests └── AsyncOperationsTests │ ├── Utils │ ├── ConcurrentTaskEvent.swift │ └── EventPublisher.swift │ ├── Sequence │ ├── SequenceAsyncFilterTests.swift │ ├── SequenceAsyncFlatMapTeats.swift │ ├── SequenceAsyncCompactMapTests.swift │ ├── SequenceAsyncContainsTests.swift │ ├── SequenceAsyncAllSatisfyTests.swift │ ├── SequenceAsyncFirstTests.swift │ ├── SequenceAsyncReduceTests.swift │ ├── SequenceAsyncMapTests.swift │ └── SequenceAsyncForEachTests.swift │ ├── WithOrderedTaskGroupTests.swift │ ├── AsyncSequence │ └── AsyncSequenceAsyncForEachTests.swift │ └── WithThrowingOrderedTaskGroupTests.swift ├── .gitignore ├── Package.swift ├── .github └── workflows │ ├── test.yml │ └── docc.yml ├── LICENSE └── README.md /Sources/AsyncOperations/numberOfConcurrentTasks.swift: -------------------------------------------------------------------------------- 1 | public let numberOfConcurrentTasks: UInt = 1 2 | -------------------------------------------------------------------------------- /Tests/AsyncOperationsTests/Utils/ConcurrentTaskEvent.swift: -------------------------------------------------------------------------------- 1 | enum ConcurrentTaskEvent: String { 2 | case start 3 | case end 4 | } 5 | -------------------------------------------------------------------------------- /Sources/AsyncOperations/Index.swift: -------------------------------------------------------------------------------- 1 | typealias Index = UInt64 2 | 3 | extension Index { 4 | func next() -> Self { 5 | self + 1 % Self.max 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Tests/AsyncOperationsTests/Sequence/SequenceAsyncFilterTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncOperations 2 | import Testing 3 | 4 | struct SequenceAsyncFilterTests { 5 | 6 | @Test 7 | func asyncFilter() async throws { 8 | let filteredNumbers = await [0, 1, 2, 3, 4].asyncFilter { number in 9 | await Task.yield() 10 | return number.isMultiple(of: 2) 11 | } 12 | #expect(filteredNumbers == [0, 2, 4]) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/AsyncOperationsTests/Sequence/SequenceAsyncFlatMapTeats.swift: -------------------------------------------------------------------------------- 1 | import AsyncOperations 2 | import Testing 3 | 4 | struct SequenceAsyncFlatMapTeats { 5 | 6 | @Test 7 | func asyncFlatMap() async throws { 8 | let results = await [0, 1, 2, 3, 4].asyncFlatMap { number in 9 | await Task.yield() 10 | return [number, number * 2] 11 | } 12 | #expect(results == [0, 0, 1, 2, 2, 4, 3, 6, 4, 8]) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/AsyncOperationsTests/Sequence/SequenceAsyncCompactMapTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncOperations 2 | import Testing 3 | 4 | struct SequenceAsyncCompactMapTests { 5 | @Test func asyncCompactMapMultipleTasks() async throws { 6 | let results = await [0, 1, 2, 3, 4].asyncCompactMap { number in 7 | await Task.yield() 8 | return number % 2 == 0 ? nil : number * 2 9 | } 10 | 11 | #expect(results == [2, 6]) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/AsyncOperationsTests/Sequence/SequenceAsyncContainsTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncOperations 2 | import Testing 3 | 4 | struct SequenceAsyncContainsTests { 5 | 6 | @Test 7 | func asyncContains() async throws { 8 | let containsResult = await [1, 2, 3].asyncContains { number in 9 | #expect(number != 3) 10 | return number == 2 11 | } 12 | #expect(containsResult) 13 | 14 | let notContainsResult = await [1, 2, 3].asyncContains { number in 15 | return number == 4 16 | } 17 | #expect(!notContainsResult) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/AsyncOperationsTests/Sequence/SequenceAsyncAllSatisfyTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncOperations 2 | import Testing 3 | 4 | struct SequenceAsyncAllSatisfyTests { 5 | @Test 6 | func asyncAllSatisfy() async throws { 7 | let satisfiedResult = await [0, 1, 2, 3, 4].asyncAllSatisfy { number in 8 | await Task.yield() 9 | return number < 5 10 | } 11 | #expect(satisfiedResult) 12 | 13 | let unsatisfiedResult = await [0, 1, 2, 3, 4].asyncAllSatisfy { number in 14 | await Task.yield() 15 | return number < 4 16 | } 17 | #expect(!unsatisfiedResult) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/AsyncOperationsTests/Sequence/SequenceAsyncFirstTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncOperations 2 | import Testing 3 | 4 | struct SequenceAsyncFirstTests { 5 | 6 | @Test 7 | func asyncFirst() async throws { 8 | let containResult = await [0, 1, 2, 3, 4].asyncFirst(numberOfConcurrentTasks: 8) { number in 9 | await Task.yield() 10 | return number % 2 == 1 11 | } 12 | #expect(containResult == 1) 13 | 14 | let notContainResult = await [0, 1, 2, 3, 4].asyncFirst { number in 15 | await Task.yield() 16 | return number == 5 17 | } 18 | #expect(notContainResult == nil) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/AsyncOperationsTests/Sequence/SequenceAsyncReduceTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncOperations 2 | import Testing 3 | 4 | @Suite 5 | struct SequenceAsyncReduceTests { 6 | @Test 7 | func asyncReduce() async throws { 8 | let results = await [1, 2, 3, 4, 5].asyncReduce(0) { result, element in 9 | await Task.yield() 10 | return result + element 11 | } 12 | #expect(results == 15) 13 | } 14 | 15 | 16 | @Test 17 | func asyncReduceInto() async throws { 18 | let results = await [1, 2, 3, 4, 5].asyncReduce(into: 0) { result, element in 19 | await Task.yield() 20 | result += element 21 | } 22 | #expect(results == 15) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/AsyncOperationsTests/WithOrderedTaskGroupTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncOperations 2 | import Testing 3 | 4 | struct WithOrderedTaskGroupTests { 5 | 6 | @Test 7 | func testWithOrderedTaskGroup() async throws { 8 | let results = await withOrderedTaskGroup(of: Int.self) { group in 9 | (0..<10).forEach { number in 10 | group.addTask { 11 | await Task.yield() 12 | return number 13 | } 14 | } 15 | var results: [Int] = [] 16 | for await number in group { 17 | results.append(number) 18 | } 19 | return results 20 | } 21 | 22 | #expect(results == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/AsyncOperationsTests/Utils/EventPublisher.swift: -------------------------------------------------------------------------------- 1 | final actor EventPublisher { 2 | private var alreadyPublished: Set = [] 3 | private var continuations: [Int: CheckedContinuation] = [:] 4 | 5 | func send(_ key: Int) { 6 | alreadyPublished.insert(key) 7 | if let continuation = continuations[key] { 8 | continuation.resume() 9 | self.continuations[key] = nil 10 | } 11 | } 12 | 13 | func wait(for key: Int) async { 14 | await withCheckedContinuation { continuation in 15 | if alreadyPublished.contains(key) { 16 | continuation.resume() 17 | } else { 18 | continuations[key] = continuation 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | import Foundation 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "swift-async-operations", 7 | platforms: [.macOS(.v13), .iOS(.v16), .watchOS(.v9), .macCatalyst(.v16), .tvOS(.v16), .visionOS(.v1)], 8 | products: [ 9 | .library(name: "AsyncOperations", targets: ["AsyncOperations"]), 10 | ], 11 | targets: [ 12 | .target(name: "AsyncOperations"), 13 | .testTarget(name: "AsyncOperationsTests", dependencies: ["AsyncOperations"]), 14 | ] 15 | ) 16 | 17 | let isDocCBuild = ProcessInfo.processInfo.environment["DOCC_BUILD"] == "1" 18 | if isDocCBuild { 19 | package.dependencies += [ 20 | .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"), 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /Sources/AsyncOperations/Sequence/Sequence+AsyncReduce.swift: -------------------------------------------------------------------------------- 1 | extension Sequence where Element: Sendable { 2 | /// An async function of `reduce`. 3 | public func asyncReduce( 4 | _ initialResult: Result, 5 | _ nextPartialResult: (Result, Element) async throws -> Result 6 | ) async rethrows -> Result { 7 | var result = initialResult 8 | 9 | for element in self { 10 | result = try await nextPartialResult(result, element) 11 | } 12 | 13 | return result 14 | } 15 | 16 | /// An async function of `reduce`. 17 | public func asyncReduce( 18 | into initialResult: Result, 19 | _ updateAccumulatingResult: (inout Result, Element) async throws -> () 20 | ) async rethrows -> Result { 21 | var result = initialResult 22 | 23 | for element in self { 24 | try await updateAccumulatingResult(&result, element) 25 | } 26 | 27 | return result 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | swift_version: 14 | - "6.0" 15 | - "6.1" 16 | - "6.2" 17 | runs-on: macos-latest 18 | steps: 19 | - name: Install Swiftly 20 | run: | 21 | curl -O https://download.swift.org/swiftly/darwin/swiftly.pkg 22 | installer -pkg swiftly.pkg -target CurrentUserHomeDirectory 23 | ~/.swiftly/bin/swiftly init --quiet-shell-followup --skip-install 24 | . "${SWIFTLY_HOME_DIR:-$HOME/.swiftly}/env.sh" 25 | hash -r 26 | - name: Add Swiftly to PATH 27 | run: echo "${HOME}/.swiftly/bin" >> "${GITHUB_PATH}" 28 | - name: Install Swift 29 | run: ~/.swiftly/bin/swiftly install --use ${{ matrix.swift_version }} 30 | - name: Get swift version 31 | run: swift --version 32 | - uses: actions/checkout@v4 33 | - name: Run tests 34 | run: swift test 35 | -------------------------------------------------------------------------------- /Sources/AsyncOperations/Sequence/Sequence+AsyncForEach.swift: -------------------------------------------------------------------------------- 1 | extension Sequence where Element: Sendable { 2 | /// An async function of `forEach`. 3 | /// - Parameters: 4 | /// - numberOfConcurrentTasks: A number of concurrent tasks. the given `body` closure run in parallel when the value is 2 or more. 5 | /// - priority: A priority of the giving closure. 6 | /// - body: A similar closure with `forEach`'s one, but it's async. 7 | public func asyncForEach( 8 | numberOfConcurrentTasks: UInt = numberOfConcurrentTasks, 9 | priority: TaskPriority? = nil, 10 | _ body: @escaping @Sendable (Element) async throws -> Void 11 | ) async rethrows { 12 | try await withThrowingTaskGroup(of: (Void, Int).self) { group in 13 | try await internalForEach( 14 | group: &group, 15 | numberOfConcurrentTasks: numberOfConcurrentTasks, 16 | priority: priority, 17 | taskOperation: body, 18 | nextOperation: { /* Do nothing */ } 19 | ) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 matsuji 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Sources/AsyncOperations/Sequence/Sequence+AsyncMap.swift: -------------------------------------------------------------------------------- 1 | extension Sequence where Element: Sendable { 2 | /// An async function of `map`. 3 | /// - Parameters: 4 | /// - numberOfConcurrentTasks: A number of concurrent tasks. the given `transform` closure run in parallel when the value is 2 or more. 5 | /// - priority: A priority of the giving closure. 6 | /// - transform: A similar closure with `map`'s one, but it's async. 7 | /// - Returns: A transformed array. 8 | public func asyncMap( 9 | numberOfConcurrentTasks: UInt = numberOfConcurrentTasks, 10 | priority: TaskPriority? = nil, 11 | _ transform: @escaping @Sendable (Element) async throws -> T 12 | ) async rethrows -> [T] { 13 | try await withThrowingTaskGroup(of: (T, Int).self) { group in 14 | var values: [T] = [] 15 | 16 | try await internalForEach( 17 | group: &group, 18 | numberOfConcurrentTasks: numberOfConcurrentTasks, 19 | priority: priority, 20 | taskOperation: transform 21 | ) { value in 22 | values.append(value) 23 | } 24 | 25 | return values 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/AsyncOperationsTests/AsyncSequence/AsyncSequenceAsyncForEachTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncOperations 2 | import Testing 3 | 4 | struct AsyncSequenceAsyncForEachTests { 5 | 6 | @Test(.timeLimit(.minutes(1))) 7 | @MainActor 8 | func asyncForEach() async throws { 9 | var results: [Int] = [] 10 | var events: [ConcurrentTaskEvent] = [] 11 | 12 | let asyncSequence = AsyncStream { c in 13 | (0..<5).forEach { c.yield($0) } 14 | c.finish() 15 | } 16 | 17 | let eventPublisher = EventPublisher() 18 | 19 | Task { 20 | for number in [2, 1, 0, 3, 4] { 21 | await eventPublisher.send(number) 22 | try await Task.sleep(for: .milliseconds(100)) 23 | } 24 | } 25 | await asyncSequence.asyncForEach(numberOfConcurrentTasks: 3) { @MainActor number in 26 | events.append(.start) 27 | await eventPublisher.wait(for: number) 28 | events.append(.end) 29 | results.append(number) 30 | } 31 | #expect(results.count == 5) 32 | #expect(Set(results) == [0, 1, 2, 3, 4]) 33 | #expect(events == [.start, .start, .start, .end, .start, .end, .start, .end, .end, .end]) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/AsyncOperations/Sequence/Sequence+AsyncFlatMap.swift: -------------------------------------------------------------------------------- 1 | extension Sequence where Element: Sendable { 2 | /// An async function of `flatMap`. 3 | /// - Parameters: 4 | /// - numberOfConcurrentTasks: A number of concurrent tasks. the given `transform` closure run in parallel when the value is 2 or more. 5 | /// - priority: A priority of the giving closure. 6 | /// - transform: A similar closure with `flatMap`'s one, but it's async. 7 | /// - Returns: A transformed array. 8 | public func asyncFlatMap( 9 | numberOfConcurrentTasks: UInt = numberOfConcurrentTasks, 10 | priority: TaskPriority? = nil, 11 | _ transform: @escaping @Sendable (Element) async throws -> [T] 12 | ) async rethrows -> [T] { 13 | try await withThrowingTaskGroup(of: ([T], Int).self) { group in 14 | var values: [T] = [] 15 | 16 | try await internalForEach( 17 | group: &group, 18 | numberOfConcurrentTasks: numberOfConcurrentTasks, 19 | priority: priority, 20 | taskOperation: transform 21 | ) { results in 22 | values.append(contentsOf: results) 23 | } 24 | 25 | return values 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/AsyncOperations/Sequence/Sequence+AsyncCompactMap.swift: -------------------------------------------------------------------------------- 1 | extension Sequence where Element: Sendable { 2 | /// An async function of `compactMap`. 3 | /// - Parameters: 4 | /// - numberOfConcurrentTasks: A number of concurrent tasks. the given `transform` closure run in parallel when the value is 2 or more. 5 | /// - priority: A priority of the giving closure. 6 | /// - transform: A similar closure with `compactMap`'s one, but it's async. 7 | /// - Returns: A transformed array which doesn't contain `nil`. 8 | public func asyncCompactMap( 9 | numberOfConcurrentTasks: UInt = numberOfConcurrentTasks, 10 | priority: TaskPriority? = nil, 11 | _ transform: @escaping @Sendable (Element) async throws -> T? 12 | ) async rethrows -> [T] { 13 | try await withThrowingTaskGroup(of: (T?, Int).self) { group in 14 | var values: [T] = [] 15 | 16 | try await internalForEach( 17 | group: &group, 18 | numberOfConcurrentTasks: numberOfConcurrentTasks, 19 | priority: priority, 20 | taskOperation: transform 21 | ) { value in 22 | guard let value else { return } 23 | values.append(value) 24 | } 25 | return values 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/AsyncOperations/Sequence/InternalForEach.swift: -------------------------------------------------------------------------------- 1 | extension Sequence where Element: Sendable { 2 | func internalForEach( 3 | group: inout ThrowingTaskGroup<(T, Int), any Error>, 4 | numberOfConcurrentTasks: UInt, 5 | priority: TaskPriority?, 6 | taskOperation: @escaping @Sendable (Element) async throws -> T, 7 | nextOperation: (T) -> () 8 | ) async throws { 9 | var currentIndex = 0 10 | var results: [Int: T] = [:] 11 | 12 | func doNextOperationIfNeeded() { 13 | while let result = results[currentIndex] { 14 | let index = currentIndex 15 | nextOperation(result) 16 | currentIndex += 1 17 | results.removeValue(forKey: index) 18 | } 19 | } 20 | 21 | for (index, element) in self.enumerated() { 22 | if numberOfConcurrentTasks <= index { 23 | if let (value, index) = try await group.next() { 24 | results[index] = value 25 | doNextOperationIfNeeded() 26 | } 27 | } 28 | group.addTask(priority: priority) { 29 | try await (taskOperation(element), index) 30 | } 31 | } 32 | 33 | for try await (value, index) in group { 34 | results[index] = value 35 | doNextOperationIfNeeded() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/docc.yml: -------------------------------------------------------------------------------- 1 | name: DocC 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | BuildDocC: 8 | runs-on: macos-15 9 | env: 10 | DEVELOPER_DIR: "/Applications/Xcode_16.2.app/Contents/Developer" 11 | steps: 12 | - uses: SwiftyLab/setup-swift@latest 13 | with: 14 | swift-version: "6.0" 15 | - uses: actions/checkout@v4 16 | - name: Build DocC 17 | run: | 18 | swift package --allow-writing-to-directory ./docs generate-documentation \ 19 | --target AsyncOperations \ 20 | --disable-indexing \ 21 | --output-path ./docs \ 22 | --transform-for-static-hosting \ 23 | --hosting-base-path swift-async-operations \ 24 | --source-service github \ 25 | --source-service-base-url https://github.com/mtj0928/swift-async-operations/blob/main \ 26 | --checkout-path $GITHUB_WORKSPACE 27 | env: 28 | DOCC_BUILD: 1 29 | - uses: actions/upload-pages-artifact@v3 30 | id: docs 31 | with: 32 | path: docs 33 | DeployDocC: 34 | needs: BuildDocC 35 | permissions: 36 | pages: write 37 | id-token: write 38 | environment: 39 | name: github-pages 40 | url: ${{ steps.deployment.outputs.page_url }} 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Deploy to GitHub Pages 44 | id: docs 45 | uses: actions/deploy-pages@v4 46 | -------------------------------------------------------------------------------- /Sources/AsyncOperations/Sequence/Sequence+AsyncFilter.swift: -------------------------------------------------------------------------------- 1 | extension Sequence where Element: Sendable { 2 | /// An async function of `filter`. 3 | /// - Parameters: 4 | /// - numberOfConcurrentTasks: A number of concurrent tasks. the given `isIncluded` closure run in parallel when the value is 2 or more. 5 | /// - priority: A priority of the giving closure. 6 | /// - isIncluded: A similar closure with `filter`'s one, but it's async. 7 | /// - Returns: A filtered array which has only elements which satisfy the `isIncluded`. 8 | public func asyncFilter( 9 | numberOfConcurrentTasks: UInt = numberOfConcurrentTasks, 10 | priority: TaskPriority? = nil, 11 | _ isIncluded: @escaping @Sendable (Element) async throws -> Bool 12 | ) async rethrows -> [Element] { 13 | try await withThrowingTaskGroup(of: (Element?, Int).self) { group in 14 | var values: [Element] = [] 15 | 16 | try await internalForEach( 17 | group: &group, 18 | numberOfConcurrentTasks: numberOfConcurrentTasks, 19 | priority: priority, 20 | taskOperation: { 21 | value in try await isIncluded(value) ? value : nil 22 | } 23 | ) { value in 24 | if let value { 25 | values.append(value) 26 | } 27 | } 28 | 29 | return values 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/AsyncOperationsTests/Sequence/SequenceAsyncMapTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncOperations 2 | import Foundation 3 | import Testing 4 | 5 | struct SequenceAsyncMapTests { 6 | 7 | @Test 8 | func asyncMapMultipleTasks() async throws { 9 | let startTime = Date() 10 | let results = try await [0, 1, 2, 3, 4].asyncMap(numberOfConcurrentTasks: 8) { number in 11 | try await Task.sleep(for: .seconds(1)) 12 | return number * 2 13 | } 14 | 15 | let endTime = Date() 16 | let difference = endTime.timeIntervalSince(startTime) 17 | 18 | #expect(results == [0, 2, 4, 6, 8]) 19 | #expect(difference < 2) 20 | } 21 | 22 | @Test 23 | func asyncMapMultipleTasksWithNumberOfConcurrentTasks() async throws { 24 | let counter = Counter() 25 | let results = await [0, 1, 2, 3, 4].asyncMap(numberOfConcurrentTasks: 2) { number in 26 | await counter.increment() 27 | let numberOfConcurrentTasks = await counter.number 28 | #expect(numberOfConcurrentTasks <= 2) 29 | await counter.decrement() 30 | return number * 2 31 | } 32 | 33 | #expect(results == [0, 2, 4, 6, 8]) 34 | } 35 | } 36 | 37 | // MARK: - private 38 | 39 | private actor Counter { 40 | private(set) var number = 0 41 | 42 | func increment() { 43 | number += 1 44 | } 45 | 46 | func decrement() { 47 | number -= 1 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/AsyncOperations/Sequence/Sequence+AsyncAllSatisfy.swift: -------------------------------------------------------------------------------- 1 | extension Sequence where Element: Sendable { 2 | /// An async function of `allSatisfy`. 3 | /// - Parameters: 4 | /// - numberOfConcurrentTasks: A number of concurrent tasks. the given `predicate` closure run in parallel when the value is 2 or more. 5 | /// - priority: A priority of the giving closure. 6 | /// - predicate: A similar closure with `allSatisfy`'s one, but it's async. 7 | /// - Returns: `true` if all elements satisfy the `predicate`. `false` if not. 8 | public func asyncAllSatisfy( 9 | numberOfConcurrentTasks: UInt = numberOfConcurrentTasks, 10 | priority: TaskPriority? = nil, 11 | _ predicate: @escaping @Sendable (Element) async throws -> Bool 12 | ) async rethrows -> Bool { 13 | try await withThrowingTaskGroup(of: Bool.self) { group in 14 | for (index, element) in self.enumerated() { 15 | if index >= numberOfConcurrentTasks { 16 | if let isSatisfy = try await group.next(), 17 | !isSatisfy { 18 | group.cancelAll() 19 | return false 20 | } 21 | } 22 | 23 | group.addTask(priority: priority) { 24 | try await predicate(element) 25 | } 26 | } 27 | 28 | for try await isSatisfy in group where !isSatisfy { 29 | group.cancelAll() 30 | return false 31 | } 32 | 33 | return true 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/AsyncOperations/Sequence/Sequence+AsyncContains.swift: -------------------------------------------------------------------------------- 1 | extension Sequence where Element: Sendable { 2 | /// An async function of `contains`. 3 | /// - Parameters: 4 | /// - numberOfConcurrentTasks: A number of concurrent tasks. the given `predicate` closure run in parallel when the value is 2 or more. 5 | /// - priority: A priority of the giving closure. 6 | /// - predicate: A similar closure with `contains`'s one, but it's async. 7 | /// - Returns: `true` if this array contains an element satisfies the given predicate. `false` if not. 8 | public func asyncContains( 9 | numberOfConcurrentTasks: UInt = numberOfConcurrentTasks, 10 | priority: TaskPriority? = nil, 11 | where predicate: @escaping @Sendable (Element) async throws -> Bool 12 | ) async rethrows -> Bool { 13 | try await withThrowingTaskGroup(of: Bool.self) { group in 14 | for (index, element) in self.enumerated() { 15 | if index >= numberOfConcurrentTasks { 16 | if let contain = try await group.next(), 17 | contain { 18 | group.cancelAll() 19 | return true 20 | } 21 | } 22 | 23 | group.addTask(priority: priority) { 24 | try await predicate(element) 25 | } 26 | } 27 | 28 | for try await contain in group where contain { 29 | group.cancelAll() 30 | return true 31 | } 32 | 33 | return false 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/AsyncOperations/Sequence/Sequence+AsyncFirst.swift: -------------------------------------------------------------------------------- 1 | extension Sequence where Element: Sendable { 2 | /// An async function of `first`. 3 | /// - Parameters: 4 | /// - numberOfConcurrentTasks: A number of concurrent tasks. the given `predicate` closure run in parallel when the value is 2 or more. 5 | /// - priority: A priority of the giving closure. 6 | /// - predicate: A similar closure with `first`'s one, but it's async. 7 | /// - Returns: A first element which satisfy the given predicate. 8 | /// 9 | /// > Note: If `numberOfConcurrentTasks` is 2 or more, the predicate closure may run for elements after the first element. 10 | public func asyncFirst( 11 | numberOfConcurrentTasks: UInt = numberOfConcurrentTasks, 12 | priority: TaskPriority? = nil, 13 | where predicate: @escaping @Sendable (Element) async throws -> Bool 14 | ) async rethrows -> Element? { 15 | try await withThrowingOrderedTaskGroup(of: Element?.self) { group in 16 | for (index, element) in self.enumerated() { 17 | if index >= numberOfConcurrentTasks { 18 | if let result = try await group.next(), result != nil { 19 | group.cancelAll() 20 | return result 21 | } 22 | } 23 | 24 | group.addTask(priority: priority) { 25 | try await predicate(element) ? element : nil 26 | } 27 | } 28 | 29 | for try await result in group where result != nil { 30 | group.cancelAll() 31 | return result 32 | } 33 | 34 | return nil 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/AsyncOperationsTests/Sequence/SequenceAsyncForEachTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncOperations 2 | import Testing 3 | 4 | @Suite 5 | struct SequenceAsyncForEachTests { 6 | @Test 7 | @MainActor 8 | func asyncForEach() async throws { 9 | var results: [Int] = [] 10 | try await [0, 1, 2, 3, 4].asyncForEach { @MainActor number in 11 | try await Task.sleep(for: .milliseconds(100 * (5 - number))) 12 | results.append(number) 13 | } 14 | #expect(results == [0, 1, 2, 3, 4]) 15 | } 16 | 17 | @Test(.timeLimit(.minutes(1))) 18 | @MainActor 19 | func asyncForEachConcurrently() async throws { 20 | var events: [ConcurrentTaskEvent] = [] 21 | let numberOfElements = 10 22 | let numberOfConcurrentTasks = 3 23 | let publisher = EventPublisher() 24 | 25 | Task { 26 | let publishOrder = [2, 1, 0, 3, 7, 5, 8, 6, 9, 4] 27 | for number in publishOrder { 28 | await publisher.send(number) 29 | try await Task.sleep(for: .milliseconds(100)) 30 | } 31 | } 32 | 33 | await (0.. Void 34 | ) async rethrows { 35 | try await withThrowingTaskGroup(of: Void.self) { group in 36 | var counter = 0 37 | var asyncIterator = self.makeAsyncIterator() 38 | while let element = try await asyncIterator.next() { 39 | if counter < numberOfConcurrentTasks { 40 | group.addTask(priority: priority) { 41 | try await body(element) 42 | } 43 | counter += 1 44 | } else { 45 | try await group.next() 46 | group.addTask(priority: priority) { 47 | try await body(element) 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/AsyncOperationsTests/WithThrowingOrderedTaskGroupTests.swift: -------------------------------------------------------------------------------- 1 | import AsyncOperations 2 | import Testing 3 | 4 | struct WithThrowingOrderedTaskGroupTests { 5 | @Test 6 | func testWithThrowingOrderedTaskGroup() async throws { 7 | let results = try await withThrowingOrderedTaskGroup(of: Int.self) { group in 8 | (0..<10).forEach { number in 9 | group.addTask { 10 | await Task.yield() 11 | return number 12 | } 13 | } 14 | var results: [Int] = [] 15 | for try await number in group { 16 | results.append(number) 17 | } 18 | return results 19 | } 20 | 21 | #expect(results == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) 22 | } 23 | 24 | @Test 25 | func withThrowingOrderedTaskGroupOnFailure() async throws { 26 | await #expect(throws: DummyError.self) { 27 | _ = try await withThrowingOrderedTaskGroup(of: Int.self) { group in 28 | (0..<10).forEach { number in 29 | group.addTask { 30 | await Task.yield() 31 | if number == 5 { throw DummyError() } 32 | return number 33 | } 34 | } 35 | var results: [Int] = [] 36 | for try await number in group { 37 | results.append(number) 38 | } 39 | return results 40 | } 41 | } 42 | } 43 | 44 | @Test 45 | func withThrowingOrderedTaskGroupByIgnoreError() async throws { 46 | let results = try await withThrowingOrderedTaskGroup(of: Int.self) { group in 47 | (0..<10).forEach { number in 48 | group.addTask { 49 | await Task.yield() 50 | if number == 5 { throw DummyError() } 51 | return number 52 | } 53 | } 54 | var results: [Int] = [] 55 | do { 56 | for try await number in group { 57 | results.append(number) 58 | } 59 | } catch is DummyError { 60 | // Expected 61 | } 62 | return results 63 | } 64 | #expect(results == [0, 1, 2, 3, 4]) 65 | } 66 | } 67 | 68 | private struct DummyError: Error {} 69 | -------------------------------------------------------------------------------- /Sources/AsyncOperations/withOrderedTaskGroup.swift: -------------------------------------------------------------------------------- 1 | /// A wrapper function of `withTaskGroup`. 2 | /// 3 | /// The main difference with `withTaskGroup` is that the group's next function returns the results in the order the tasks were added. 4 | /// 5 | /// ```swift 6 | /// let results = await withOrderedTaskGroup(of: Int.self) { group in 7 | /// (0..<5).forEach { number in 8 | /// group.addTask { 9 | /// await Task.yield() 10 | /// return number * 2 11 | /// } 12 | /// } 13 | /// var results: [Int] = [] 14 | /// for await number in group { 15 | /// results.append(number) 16 | /// } 17 | /// return results 18 | /// } 19 | /// print(result) // [0, 2, 4, 6, 8, 10] 20 | /// ``` 21 | public func withOrderedTaskGroup( 22 | of childTaskResultType: ChildTaskResult.Type, 23 | returning returnType: GroupResult.Type = GroupResult.self, 24 | isolation: isolated (any Actor)? = #isolation, 25 | body: (inout OrderedTaskGroup) async -> GroupResult 26 | ) async -> GroupResult { 27 | await withTaskGroup(of: (Index, ChildTaskResult).self, returning: returnType) { group in 28 | var orderedTaskGroup = OrderedTaskGroup(group) 29 | return await body(&orderedTaskGroup) 30 | } 31 | } 32 | 33 | public struct OrderedTaskGroup { 34 | private var internalGroup: TaskGroup<(Index, ChildTaskResult)> 35 | private var addedTaskIndex: Index = 0 36 | private var nextIndex: Index = 0 37 | private var unreturnedResults: [Index: ChildTaskResult] = [:] 38 | 39 | fileprivate init(_ internalGroup: TaskGroup<(Index, ChildTaskResult)>) { 40 | self.internalGroup = internalGroup 41 | } 42 | 43 | public mutating func addTask( 44 | priority: TaskPriority? = nil, 45 | operation: sending @escaping @isolated(any) () async -> ChildTaskResult 46 | ) { 47 | let currentIndex = addedTaskIndex 48 | internalGroup.addTask(priority: priority) { 49 | let result = await operation() 50 | return (currentIndex, result) 51 | } 52 | addedTaskIndex = addedTaskIndex.next() 53 | } 54 | 55 | public mutating func waitForAll() async { 56 | await internalGroup.waitForAll() 57 | } 58 | 59 | public func cancelAll() { 60 | internalGroup.cancelAll() 61 | } 62 | } 63 | 64 | extension OrderedTaskGroup: AsyncSequence, AsyncIteratorProtocol { 65 | public typealias Element = ChildTaskResult 66 | public typealias Failure = Never 67 | 68 | public func makeAsyncIterator() -> Self { 69 | self 70 | } 71 | 72 | public mutating func next() async -> ChildTaskResult? { 73 | if let result = unreturnedResults[nextIndex] { 74 | unreturnedResults.removeValue(forKey: nextIndex) 75 | nextIndex = nextIndex.next() 76 | return result 77 | } 78 | 79 | if let (index, result) = await internalGroup.next() { 80 | unreturnedResults[index] = result 81 | return await next() 82 | } 83 | 84 | return nil 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/AsyncOperations/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``AsyncOperations`` 2 | 3 | Adds the capability of async operations. 4 | 5 | ## Overview 6 | `AsyncOperations` provides asynchronous operations for generic higher-order functions like `map` and `filter`. 7 | 8 | This library provides two features. 9 | 10 | 1. Async functions of `Sequence`. 11 | 2. Ordered Task Group 12 | 13 | ### Async functions of `Sequence`. 14 | Original functions of `Sequence` don't support Swift Concurrency and 15 | async function is not available in like a `forEach` closure. 16 | ```swift 17 | [1, 2, 3].forEach { number in 18 | // 😢 async function is not available here. 19 | } 20 | ``` 21 | 22 | This library provides async operations like `asyncForEach` and `asyncMap`. 23 | 24 | ```swift 25 | try await [1, 2, 3].asyncForEach { number in 26 | print("Start: \(number)") 27 | try await doSomething(number) // 😁 async function is available here. 28 | print("End: \(number)") 29 | } 30 | ``` 31 | 32 | The closure runs sequential as a default behavior. 33 | 34 | ``` 35 | Start: 1 36 | End: 1 37 | Start: 2 38 | End: 2 39 | Start: 3 40 | End: 3 41 | ``` 42 | 43 | As an advanced usage, `numberOfConcurrentTasks` can be specified and the closure can run in parallel if the value is 2 or more. 44 | ```swift 45 | try await [1, 2, 3].asyncForEach(numberOfConcurrentTasks: 3) { number in 46 | print("Start: \(number)") 47 | try await doSomething(number) 48 | print("End: \(number)") 49 | } 50 | ``` 51 | 52 | ``` 53 | Start: 2 54 | End: 2 55 | Start: 1 56 | Start: 3 57 | End: 3 58 | End: 1 59 | ``` 60 | 61 | The extended functions perform parallel execution even for order-sensitive functions like `map` function, 62 | transforming the array while preserving the original order. 63 | ```swift 64 | let result = try await [1, 2, 3].asyncMap(numberOfConcurrentTasks: 3) { number in 65 | print("Start: \(number)") 66 | let result = try await twice(number) 67 | print("End: \(number)") 68 | return result 69 | } 70 | print(result) 71 | ``` 72 | 73 | ``` 74 | Start: 1 75 | Start: 3 76 | End: 3 77 | End: 1 78 | Start: 2 79 | End: 2 80 | [2, 4, 6] 81 | ``` 82 | This library provides 83 | - `asyncForEach` 84 | - `asyncMap` 85 | - `asyncFlatMap` 86 | - `asyncCompactMap` 87 | - `asyncFilter` 88 | - `asyncFirst` 89 | - `asyncAllSatisfy` 90 | - `asyncContains` 91 | - `asyncReduce` 92 | 93 | ### Ordered Task Group 94 | The original utility function `withTaskGroup` and `withThrowingTaskGroup` don't ensure the order of `for await`. 95 | ```swift 96 | let results = await withTaskGroup(of: Int.self) { group in 97 | (0..<5).forEach { number in 98 | group.addTask { 99 | await Task.yield() 100 | return number * 2 101 | } 102 | } 103 | var results: [Int] = [] 104 | for await number in group { 105 | results.append(number) 106 | } 107 | return results 108 | } 109 | print(results) // ☹️ [0, 4, 2, 6, 10, 8] 110 | ``` 111 | 112 | However, ordered `for await` is required in some of situations like converting an array to a new array. 113 | 114 | `withOrderedTaskGroup` and `withThrowingOrderedTaskGroup` satisfy such requirements. 115 | ```swift 116 | let results = await withOrderedTaskGroup(of: Int.self) { group in 117 | (0..<5).forEach { number in 118 | group.addTask { 119 | await Task.yield() 120 | return number * 2 121 | } 122 | } 123 | var results: [Int] = [] 124 | for await number in group { 125 | results.append(number) 126 | } 127 | return results 128 | } 129 | print(results) // 😁 [0, 2, 4, 6, 8, 10] 130 | ``` 131 | 132 | -------------------------------------------------------------------------------- /Sources/AsyncOperations/withThrowingOrderedTaskGroup.swift: -------------------------------------------------------------------------------- 1 | /// A wrapper function of `withThrowingTaskGroup`. 2 | /// 3 | /// The main difference with `withThrowingTaskGroup` is that the group's next function returns the results in the order the tasks were added. 4 | /// 5 | /// ```swift 6 | /// let results = await try withThrowingOrderedTaskGroup(of: Int.self) { group in 7 | /// (0..<5).forEach { number in 8 | /// group.addTask { 9 | /// if number > 10 { 10 | /// throw YourError() 11 | /// } 12 | /// try await Task.yield() 13 | /// return number * 2 14 | /// } 15 | /// } 16 | /// var results: [Int] = [] 17 | /// for try await number in group { 18 | /// results.append(number) 19 | /// } 20 | /// return results 21 | /// } 22 | /// print(result) // [0, 2, 4, 6, 8, 10] 23 | /// ``` 24 | public func withThrowingOrderedTaskGroup( 25 | of childTaskResultType: ChildTaskResult.Type, 26 | returning returnType: GroupResult.Type = GroupResult.self, 27 | isolation: isolated (any Actor)? = #isolation, 28 | body: (inout ThrowingOrderedTaskGroup) async throws -> GroupResult 29 | ) async rethrows -> GroupResult { 30 | try await withThrowingTaskGroup( 31 | of: (Index, ChildTaskResult).self, 32 | returning: GroupResult.self 33 | ) { group in 34 | var throwingOrderedTaskGroup = ThrowingOrderedTaskGroup(group) 35 | return try await body(&throwingOrderedTaskGroup) 36 | } 37 | } 38 | 39 | public struct ThrowingOrderedTaskGroup { 40 | private var internalGroup: ThrowingTaskGroup<(Index, ChildTaskResult), Failure> 41 | private var addedTaskIndex: Index = 0 42 | private var nextIndex: Index = 0 43 | private var unreturnedResults: [Index: Result] = [:] 44 | 45 | init(_ internalGroup: ThrowingTaskGroup<(Index, ChildTaskResult), Failure>) { 46 | self.internalGroup = internalGroup 47 | } 48 | 49 | public mutating func addTask( 50 | priority: TaskPriority? = nil, 51 | operation: sending @escaping @isolated(any) () async throws(Failure) -> ChildTaskResult 52 | ) { 53 | let currentIndex = addedTaskIndex 54 | internalGroup.addTask(priority: priority) { 55 | do throws(Failure) { 56 | let result = try await operation() 57 | return (currentIndex, result) 58 | } catch { 59 | throw InternalError(index: currentIndex, rawError: error) 60 | } 61 | } 62 | addedTaskIndex = addedTaskIndex.next() 63 | } 64 | 65 | public mutating func waitForAll() async throws { 66 | do { 67 | try await internalGroup.waitForAll() 68 | } catch let error as InternalError { 69 | throw error.rawError 70 | } 71 | } 72 | 73 | public func cancelAll() { 74 | internalGroup.cancelAll() 75 | } 76 | } 77 | 78 | extension ThrowingOrderedTaskGroup: AsyncSequence, AsyncIteratorProtocol where Failure: Error { 79 | public typealias Element = ChildTaskResult 80 | 81 | public func makeAsyncIterator() -> Self { 82 | self 83 | } 84 | 85 | public mutating func next() async throws -> ChildTaskResult? { 86 | if let result = unreturnedResults[nextIndex] { 87 | unreturnedResults.removeValue(forKey: nextIndex) 88 | nextIndex = nextIndex.next() 89 | return try result.get() 90 | } 91 | 92 | do { 93 | if let (index, result) = try await internalGroup.next() { 94 | unreturnedResults[index] = .success(result) 95 | return try await next() 96 | } 97 | } catch let error as InternalError { 98 | unreturnedResults[error.index] = .failure(error.rawError) 99 | return try await next() 100 | } 101 | 102 | return nil 103 | } 104 | } 105 | 106 | private struct InternalError: Error { 107 | var index: Index 108 | var rawError: Failure 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-async-operations 2 | A library extending the capabilities of async operations. 3 | 4 | ## Motivation 5 | Swift concurrency is a powerful language feature, but there are no convenient APIs for array operations with Swift concurrency. 6 | Developers often have to write redundant code. 7 | 8 | ```swift 9 | var results: [Int] = [] // ☹️ var is required. 10 | for await element in [0, 1, 2, 3, 4] { 11 | let newElement = try await twice(element) 12 | results.append(newElement) 13 | } 14 | print(results) // [0, 2, 4, 6, 8] 15 | ``` 16 | 17 | In a case where the loop needs to run concurrently, a developer is required to write more redundant code. 18 | ```swift 19 | // ☹️ Long redundant code 20 | let array = [0, 1, 2, 3, 4] 21 | let results = try await withThrowingTaskGroup(of: (Int, Int).self) { group in 22 | for (index, number) in array.enumerated() { 23 | group.addTask { 24 | (index, try await twice(number)) 25 | } 26 | } 27 | var results: [Int: Int] = [:] 28 | for try await (index, result) in group { 29 | results[index] = result 30 | } 31 | // ☹️ Need to take the order into account. 32 | return results.sorted(by: { $0.key < $1.key }).map(\.value) 33 | } 34 | print(results) // [0, 2, 4, 6, 8] 35 | ``` 36 | 37 | ## Solution 38 | This library provides async functions as extensions of `Sequence` like `asyncMap`. 39 | ```swift 40 | let converted = try await [0, 1, 2, 3, 4].asyncMap { number in 41 | try await twice(number) 42 | } 43 | print(converted) // [0, 2, 4, 6, 8] 44 | ``` 45 | The closure runs sequentially by default. 46 | And by specifying a max number of tasks, the closure can also run concurrently. 47 | 48 | ```swift 49 | let converted = try await [0, 1, 2, 3, 4].asyncMap(numberOfConcurrentTasks: 8) { number in 50 | try await twice(number) 51 | } 52 | print(converted) // [0, 2, 4, 6, 8] 53 | ``` 54 | 55 | ## Feature Details 56 | The library provides two features. 57 | 1. Async functions of `Sequence`. 58 | 2. Ordered Task Group 59 | 60 | ### Async functions of `Sequence` 61 | This library provides async operations like `asyncForEach` and `asyncMap`. 62 | 63 | ```swift 64 | try await [1, 2, 3].asyncForEach { number in 65 | print("Start: \(number)") 66 | try await doSomething(number) 67 | print("End: \(number)") 68 | } 69 | ``` 70 | 71 | The closure runs sequentially by default. 72 | 73 | ``` 74 | Start: 1 75 | End: 1 76 | Start: 2 77 | End: 2 78 | Start: 3 79 | End: 3 80 | ``` 81 | 82 | As an advanced usage, `numberOfConcurrentTasks` can be specified and the closure can run in parallel if the value is 2 or more. 83 | ```swift 84 | try await [1, 2, 3].asyncForEach(numberOfConcurrentTasks: 3) { number in 85 | print("Start: \(number)") 86 | try await doSomething(number) 87 | print("End: \(number)") 88 | } 89 | ``` 90 | 91 | ``` 92 | Start: 2 93 | End: 2 94 | Start: 1 95 | Start: 3 96 | End: 3 97 | End: 1 98 | ``` 99 | 100 | The extended functions perform parallel execution even for order-sensitive functions like `map` function, 101 | transforming the array while preserving the original order. 102 | ```swift 103 | let result = try await [1, 2, 3].asyncMap(numberOfConcurrentTasks: 3) { number in 104 | print("Start: \(number)") 105 | let result = try await twice(number) 106 | print("End: \(number)") 107 | return result 108 | } 109 | print(result) 110 | ``` 111 | 112 | ``` 113 | Start: 1 114 | Start: 3 115 | End: 3 116 | End: 1 117 | Start: 2 118 | End: 2 119 | [2, 4, 6] 120 | ``` 121 | This library provides 122 | - Sequence 123 | - [asyncForEach](https://mtj0928.github.io/swift-async-operations/documentation/asyncoperations/swift/sequence/asyncforeach(numberofconcurrenttasks:priority:_:)) 124 | - [asyncMap](https://mtj0928.github.io/swift-async-operations/documentation/asyncoperations/swift/sequence/asyncmap(numberofconcurrenttasks:priority:_:)) 125 | - [asyncFlatMap](https://mtj0928.github.io/swift-async-operations/documentation/asyncoperations/swift/sequence/asyncflatmap(numberofconcurrenttasks:priority:_:)) 126 | - [asyncCompactMap](https://mtj0928.github.io/swift-async-operations/documentation/asyncoperations/swift/sequence/asynccompactmap(numberofconcurrenttasks:priority:_:)) 127 | - [asyncFilter](https://mtj0928.github.io/swift-async-operations/documentation/asyncoperations/swift/sequence/asyncfilter(numberofconcurrenttasks:priority:_:)) 128 | - [asyncFirst](https://mtj0928.github.io/swift-async-operations/documentation/asyncoperations/swift/sequence/asyncfirst(numberofconcurrenttasks:priority:where:)) 129 | - [asyncAllSatisfy](https://mtj0928.github.io/swift-async-operations/documentation/asyncoperations/swift/sequence/asyncallsatisfy(numberofconcurrenttasks:priority:_:)) 130 | - [asyncContains](https://mtj0928.github.io/swift-async-operations/documentation/asyncoperations/swift/sequence/asynccontains(numberofconcurrenttasks:priority:where:)) 131 | - [asyncReduce](https://mtj0928.github.io/swift-async-operations/documentation/asyncoperations/swift/sequence/asyncreduce(into:_:)) 132 | - AsyncSequence 133 | - [asyncForEach](https://mtj0928.github.io/swift-async-operations/documentation/asyncoperations/_concurrency/asyncsequence/asyncforeach(numberofconcurrenttasks:priority:_:)) 134 | 135 | ### Ordered Task Group 136 | The original utility function `withTaskGroup` and `withThrowingTaskGroup` don't ensure the order of `for await`. 137 | ```swift 138 | let results = await withTaskGroup(of: Int.self) { group in 139 | (0..<5).forEach { number in 140 | group.addTask { 141 | await Task.yield() 142 | return number * 2 143 | } 144 | } 145 | var results: [Int] = [] 146 | for await number in group { 147 | results.append(number) 148 | } 149 | return results 150 | } 151 | print(results) // ☹️ [0, 4, 2, 6, 10, 8] 152 | ``` 153 | 154 | However, ordered `for await` is required in some situations like converting an array to a new array. 155 | 156 | [withOrderedTaskGroup](https://mtj0928.github.io/swift-async-operations/documentation/asyncoperations/withorderedtaskgroup(of:returning:isolation:body:)) and [withThrowingOrderedTaskGroup](https://mtj0928.github.io/swift-async-operations/documentation/asyncoperations/withthrowingorderedtaskgroup(of:returning:isolation:body:)) satisfy such requirements. 157 | ```swift 158 | let results = await withOrderedTaskGroup(of: Int.self) { group in 159 | (0..<5).forEach { number in 160 | group.addTask { 161 | await Task.yield() 162 | return number * 2 163 | } 164 | } 165 | var results: [Int] = [] 166 | for await number in group { 167 | results.append(number) 168 | } 169 | return results 170 | } 171 | print(results) // 😁 [0, 2, 4, 6, 8, 10] 172 | ``` 173 | 174 | ## Requirements 175 | Swift 6.0 or later. 176 | 177 | ## Installation 178 | You can install the library via Swift Package Manager. 179 | ```swift 180 | dependencies: [ 181 | .package(url: "https://github.com/mtj0928/swift-async-operations", from: "0.1.0") 182 | ] 183 | ``` 184 | 185 | ## Documentation 186 | Please see [the DocC pages](https://mtj0928.github.io/swift-async-operations/documentation/asyncoperations/) 187 | 188 | ## Achievements 189 | - Nominated for the [Swift.org Community Showcase (November 2024)](https://www.swift.org/packages/showcase-november-2024.html). 190 | --------------------------------------------------------------------------------