diff --git a/Evolution/NNN-recursiveMap.md b/Evolution/NNN-recursiveMap.md new file mode 100644 index 00000000..90fe1499 --- /dev/null +++ b/Evolution/NNN-recursiveMap.md @@ -0,0 +1,126 @@ +# recursiveMap + +* Proposal: [NNNN](NNNN-filename.md) +* Authors: [SusanDoggie](https://github.com/SusanDoggie) +* Review Manager: TBD +* Status: **Awaiting implementation** +* Implementation: [apple/swift-async-algorithms#NNNNN](https://github.com/apple/swift-async-algorithms/pull/118) + +## Introduction + +Bring SQL's recursive CTE like operation to Swift. This method traverses all nodes of the tree and produces a flat sequence. + +Swift forums thread: [[Pitch] Add recursiveMap(_:) methods](https://forums.swift.org/t/pitch-add-recursivemap-methods/56810) + +## Proposed Solution + +Produces a sequence containing the original sequence and the recursive mapped sequence. The order of ouput elements affects by the traversal option. + +```swift +struct Node { + var id: Int + var children: [Node] = [] +} +let tree = [ + Node(id: 1, children: [ + Node(id: 2), + Node(id: 3, children: [ + Node(id: 4), + ]), + Node(id: 5), + ]), + Node(id: 6), +] +for await node in tree.async.recursiveMap({ $0.children.async }) { + print(node.id) +} +// 1 +// 2 +// 3 +// 4 +// 5 +// 6 +``` + +### Traversal Option + +This function comes with two different traversal methods. This option affects the element order of the output sequence. + +- `depthFirst`: The algorithm will go down first and produce the resulting path. The algorithm starts with original + sequence and calling the supplied closure first. This is default option. + + With the structure of tree: + ```swift + let tree = [ + Node(id: 1, children: [ + Node(id: 2), + Node(id: 3, children: [ + Node(id: 4), + ]), + Node(id: 5), + ]), + Node(id: 6), + ] + ``` + + The resulting sequence will be 1 -> 2 -> 3 -> 4 -> 5 -> 6 + + The sequence using a buffer keep tracking the path of nodes. It should not using this option for searching the indefinite deep of tree. + +- `breadthFirst`: The algorithm will go through the previous sequence first and chaining all the occurring sequences. + + With the structure of tree: + ```swift + let tree = [ + Node(id: 1, children: [ + Node(id: 2), + Node(id: 3, children: [ + Node(id: 4), + ]), + Node(id: 5), + ]), + Node(id: 6), + ] + ``` + + The resulting sequence will be 1 -> 6 -> 2 -> 3 -> 5 -> 4 + + The sequence using a buffer storing occuring nodes of sequences. It should not using this option for searching the indefinite length of occuring sequences. + +## Detailed Design + +The `recursiveMap(option:_:)` method is declared as `AsyncSequence` extensions, and return `AsyncRecursiveMapSequence` or `AsyncThrowingRecursiveMapSequence` instance: + +```swift +extension AsyncSequence { + public func recursiveMap( + option: AsyncRecursiveMapSequence.TraversalOption = .depthFirst, + _ transform: @Sendable @escaping (Element) async -> S + ) -> AsyncRecursiveMapSequence + + public func recursiveMap( + option: AsyncThrowingRecursiveMapSequence.TraversalOption = .depthFirst, + _ transform: @Sendable @escaping (Element) async throws -> S + ) -> AsyncThrowingRecursiveMapSequence +} +``` + +For the non-throwing recursive map sequence, `AsyncRecursiveMapSequence` will throw only if the base sequence or the transformed sequence throws. As the opposite side, `AsyncThrowingRecursiveMapSequence` throws when the base sequence, the transformed sequence or the supplied closure throws. + +The sendability behavior of `Async[Throwing]RecursiveMapSequence` is such that when the base, base iterator, and element are `Sendable` then `Async[Throwing]RecursiveMapSequence` is `Sendable`. + +### Complexity + +Calling this method is O(_1_). + +## Effect on API resilience + +none. + +## Alternatives considered + +none. + +## Acknowledgments + +none. diff --git a/README.md b/README.md index e3c957a9..4221d1f3 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ This package is the home for these APIs. Development and API design take place o - [`adjacentPairs()`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/AdjacentPairs.md): Collects tuples of adjacent elements. - [`chunks(...)` and `chunked(...)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md): Collect values into chunks. - [`compacted()`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Compacted.md): Remove nil values from an asynchronous sequence. +- [`recursiveMap(option:_:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/RecursiveMap.md): Produces a sequence containing the original sequence and the recursive mapped sequence. The order of ouput elements affects by the traversal option. - [`removeDuplicates()`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/RemoveDuplicates.md): Remove sequentially adjacent duplicate values. - [`interspersed(with:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Intersperse.md): Place a value between every two elements of an asynchronous sequence. diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md index 766d54de..1bb9be7a 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md @@ -41,6 +41,8 @@ | `AsyncMerge2Sequence.Iterator` | rethrows | Sendable | | `AsyncMerge3Sequence` | rethrows | Sendable | | `AsyncMerge3Sequence.Iterator` | rethrows | Sendable | +| `AsyncRecursiveMapSequence` | rethrows | Conditional | +| `AsyncRecursiveMapSequence.Iterator` | rethrows | Conditional | | `AsyncRemoveDuplicatesSequence` | rethrows | Conditional | | `AsyncRemoveDuplicatesSequence.Iterator` | rethrows | Conditional | | `AsyncThrottleSequence` | rethrows | Conditional | @@ -51,6 +53,8 @@ | `AsyncThrowingExclusiveReductionsSequence.Iterator` | throws | Conditional | | `AsyncThrowingInclusiveReductionsSequence` | throws | Conditional | | `AsyncThrowingInclusiveReductionsSequence.Iterator` | throws | Conditional | +| `AsyncThrowingRecursiveMapSequence` | throws | Conditional | +| `AsyncThrowingRecursiveMapSequence.Iterator` | throws | Conditional | | `AsyncTimerSequence` | non-throwing | Sendable | | `AsyncTimerSequence.Iterator` | non-throwing | Sendable | | `AsyncZip2Sequence` | rethrows | Sendable | diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/RecursiveMap.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/RecursiveMap.md new file mode 100644 index 00000000..9d2c64e5 --- /dev/null +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/RecursiveMap.md @@ -0,0 +1,109 @@ +# RecursiveMap + +* Author(s): [Susan Cheng](https://github.com/SusanDoggie) + +[ +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncRecursiveMapSequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestRecursiveMap.swift) +] + +## Proposed Solution + +Produces a sequence containing the original sequence and the recursive mapped sequence. The order of ouput elements affects by the traversal option. + +```swift +struct Node { + var id: Int + var children: [Node] = [] +} +let tree = [ + Node(id: 1, children: [ + Node(id: 2), + Node(id: 3, children: [ + Node(id: 4), + ]), + Node(id: 5), + ]), + Node(id: 6), +] +for await node in tree.async.recursiveMap({ $0.children.async }) { + print(node.id) +} +// 1 +// 2 +// 3 +// 4 +// 5 +// 6 +``` + +### Traversal Option + +This function comes with two different traversal methods. This option affects the element order of the output sequence. + +- `depthFirst`: The algorithm will go down first and produce the resulting path. The algorithm starts with original + sequence and calling the supplied closure first. This is default option. + + With the structure of tree: + ```swift + let tree = [ + Node(id: 1, children: [ + Node(id: 2), + Node(id: 3, children: [ + Node(id: 4), + ]), + Node(id: 5), + ]), + Node(id: 6), + ] + ``` + + The resulting sequence will be 1 -> 2 -> 3 -> 4 -> 5 -> 6 + + The sequence using a buffer keep tracking the path of nodes. It should not using this option for searching the indefinite deep of tree. + +- `breadthFirst`: The algorithm will go through the previous sequence first and chaining all the occurring sequences. + + With the structure of tree: + ```swift + let tree = [ + Node(id: 1, children: [ + Node(id: 2), + Node(id: 3, children: [ + Node(id: 4), + ]), + Node(id: 5), + ]), + Node(id: 6), + ] + ``` + + The resulting sequence will be 1 -> 6 -> 2 -> 3 -> 5 -> 4 + + The sequence using a buffer storing occuring nodes of sequences. It should not using this option for searching the indefinite length of occuring sequences. + +## Detailed Design + +The `recursiveMap(option:_:)` method is declared as `AsyncSequence` extensions, and return `AsyncRecursiveMapSequence` or `AsyncThrowingRecursiveMapSequence` instance: + +```swift +extension AsyncSequence { + public func recursiveMap( + option: AsyncRecursiveMapSequence.TraversalOption = .depthFirst, + _ transform: @Sendable @escaping (Element) async -> S + ) -> AsyncRecursiveMapSequence + + public func recursiveMap( + option: AsyncThrowingRecursiveMapSequence.TraversalOption = .depthFirst, + _ transform: @Sendable @escaping (Element) async throws -> S + ) -> AsyncThrowingRecursiveMapSequence +} +``` + +For the non-throwing recursive map sequence, `AsyncRecursiveMapSequence` will throw only if the base sequence or the transformed sequence throws. As the opposite side, `AsyncThrowingRecursiveMapSequence` throws when the base sequence, the transformed sequence or the supplied closure throws. + +The sendability behavior of `Async[Throwing]RecursiveMapSequence` is such that when the base, base iterator, and element are `Sendable` then `Async[Throwing]RecursiveMapSequence` is `Sendable`. + +### Complexity + +Calling this method is O(_1_). diff --git a/Sources/AsyncAlgorithms/AsyncRecursiveMapSequence.swift b/Sources/AsyncAlgorithms/AsyncRecursiveMapSequence.swift new file mode 100644 index 00000000..2ab47a3c --- /dev/null +++ b/Sources/AsyncAlgorithms/AsyncRecursiveMapSequence.swift @@ -0,0 +1,209 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension AsyncSequence { + /// Returns a sequence containing the original sequence and the recursive mapped sequence. + /// The order of ouput elements affects by the traversal option. + /// + /// ``` + /// struct Node { + /// var id: Int + /// var children: [Node] = [] + /// } + /// let tree = [ + /// Node(id: 1, children: [ + /// Node(id: 2), + /// Node(id: 3, children: [ + /// Node(id: 4), + /// ]), + /// Node(id: 5), + /// ]), + /// Node(id: 6), + /// ] + /// for await node in tree.async.recursiveMap({ $0.children.async }) { + /// print(node.id) + /// } + /// // 1 + /// // 2 + /// // 3 + /// // 4 + /// // 5 + /// // 6 + /// ``` + /// + /// - Parameters: + /// - option: Traversal option. This option affects the element order of the output sequence. default depth-first. + /// - transform: A closure that map the element to new sequence. + /// - Returns: A sequence of the original sequence followed by recursive mapped sequence. + @inlinable + public func recursiveMap( + option: AsyncRecursiveMapSequence.TraversalOption = .depthFirst, + _ transform: @Sendable @escaping (Element) async -> C + ) -> AsyncRecursiveMapSequence { + return AsyncRecursiveMapSequence(self, option, transform) + } +} + +/// A sequence containing the original sequence and the recursive mapped sequence. +/// The order of ouput elements affects by the traversal option. +public struct AsyncRecursiveMapSequence: AsyncSequence where Base.Element == Transformed.Element { + + public typealias Element = Base.Element + + @usableFromInline + let base: Base + + @usableFromInline + let option: TraversalOption + + @usableFromInline + let transform: @Sendable (Base.Element) async -> Transformed + + @inlinable + init( + _ base: Base, + _ option: TraversalOption, + _ transform: @Sendable @escaping (Base.Element) async -> Transformed + ) { + self.base = base + self.option = option + self.transform = transform + } + + @inlinable + public func makeAsyncIterator() -> AsyncIterator { + return AsyncIterator(base, option, transform) + } +} + +extension AsyncRecursiveMapSequence { + + /// Traversal option. This option affects the element order of the output sequence. + public enum TraversalOption: Sendable { + + /// The algorithm will go down first and produce the resulting path. + case depthFirst + + /// The algorithm will go through the previous sequence first and chaining all the occurring sequences. + case breadthFirst + + } + + public struct AsyncIterator: AsyncIteratorProtocol { + + @usableFromInline + var base: Base.AsyncIterator? + + @usableFromInline + let option: TraversalOption + + @usableFromInline + var mapped: ArraySlice = [] + + @usableFromInline + var mapped_iterator: Transformed.AsyncIterator? + + @usableFromInline + let transform: @Sendable (Base.Element) async -> Transformed + + @inlinable + init( + _ base: Base, + _ option: TraversalOption, + _ transform: @Sendable @escaping (Base.Element) async -> Transformed + ) { + self.base = base.makeAsyncIterator() + self.option = option + self.transform = transform + } + + @inlinable + mutating func tryNext() async rethrows -> Base.Element? { + + switch option { + + case .depthFirst: + + while self.mapped_iterator != nil { + + if let element = try await self.mapped_iterator!.next() { + mapped.append(self.mapped_iterator!) + self.mapped_iterator = await transform(element).makeAsyncIterator() + return element + } + + self.mapped_iterator = mapped.popLast() + } + + if self.base != nil { + + if let element = try await self.base!.next() { + self.mapped_iterator = await transform(element).makeAsyncIterator() + return element + } + + self.base = nil + } + + return nil + + case .breadthFirst: + + if self.base != nil { + + if let element = try await self.base!.next() { + await mapped.append(transform(element).makeAsyncIterator()) + return element + } + + self.base = nil + self.mapped_iterator = mapped.popFirst() + } + + while self.mapped_iterator != nil { + + if let element = try await self.mapped_iterator!.next() { + await mapped.append(transform(element).makeAsyncIterator()) + return element + } + + self.mapped_iterator = mapped.popFirst() + } + + return nil + } + } + + @inlinable + public mutating func next() async rethrows -> Base.Element? { + + do { + + return try await self.tryNext() + + } catch { + + // set all state to empty + base = nil + mapped = [] + mapped_iterator = nil + + throw error + } + } + } +} + +extension AsyncRecursiveMapSequence: Sendable +where Base: Sendable, Base.Element: Sendable, Transformed: Sendable { } + +extension AsyncRecursiveMapSequence.AsyncIterator: Sendable +where Base.AsyncIterator: Sendable, Base.Element: Sendable, Transformed: Sendable, Transformed.AsyncIterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncThrowingRecursiveMapSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingRecursiveMapSequence.swift new file mode 100644 index 00000000..c98a5859 --- /dev/null +++ b/Sources/AsyncAlgorithms/AsyncThrowingRecursiveMapSequence.swift @@ -0,0 +1,209 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension AsyncSequence { + /// Returns a sequence containing the original sequence and the recursive mapped sequence. + /// The order of ouput elements affects by the traversal option. + /// + /// ``` + /// struct Node { + /// var id: Int + /// var children: [Node] = [] + /// } + /// let tree = [ + /// Node(id: 1, children: [ + /// Node(id: 2), + /// Node(id: 3, children: [ + /// Node(id: 4), + /// ]), + /// Node(id: 5), + /// ]), + /// Node(id: 6), + /// ] + /// for await node in tree.async.recursiveMap({ $0.children.async }) { + /// print(node.id) + /// } + /// // 1 + /// // 2 + /// // 3 + /// // 4 + /// // 5 + /// // 6 + /// ``` + /// + /// - Parameters: + /// - option: Traversal option. This option affects the element order of the output sequence. default depth-first. + /// - transform: A closure that map the element to new sequence. + /// - Returns: A sequence of the original sequence followed by recursive mapped sequence. + @inlinable + public func recursiveMap( + option: AsyncThrowingRecursiveMapSequence.TraversalOption = .depthFirst, + _ transform: @Sendable @escaping (Element) async throws -> C + ) -> AsyncThrowingRecursiveMapSequence { + return AsyncThrowingRecursiveMapSequence(self, option, transform) + } +} + +/// A sequence containing the original sequence and the recursive mapped sequence. +/// The order of ouput elements affects by the traversal option. +public struct AsyncThrowingRecursiveMapSequence: AsyncSequence where Base.Element == Transformed.Element { + + public typealias Element = Base.Element + + @usableFromInline + let base: Base + + @usableFromInline + let option: TraversalOption + + @usableFromInline + let transform: @Sendable (Base.Element) async throws -> Transformed + + @inlinable + init( + _ base: Base, + _ option: TraversalOption, + _ transform: @Sendable @escaping (Base.Element) async throws -> Transformed + ) { + self.base = base + self.option = option + self.transform = transform + } + + @inlinable + public func makeAsyncIterator() -> AsyncIterator { + return AsyncIterator(base, option, transform) + } +} + +extension AsyncThrowingRecursiveMapSequence { + + /// Traversal option. This option affects the element order of the output sequence. + public enum TraversalOption: Sendable { + + /// The algorithm will go down first and produce the resulting path. + case depthFirst + + /// The algorithm will go through the previous sequence first and chaining all the occurring sequences. + case breadthFirst + + } + + public struct AsyncIterator: AsyncIteratorProtocol { + + @usableFromInline + var base: Base.AsyncIterator? + + @usableFromInline + let option: TraversalOption + + @usableFromInline + var mapped: ArraySlice = [] + + @usableFromInline + var mapped_iterator: Transformed.AsyncIterator? + + @usableFromInline + let transform: @Sendable (Base.Element) async throws -> Transformed + + @inlinable + init( + _ base: Base, + _ option: TraversalOption, + _ transform: @Sendable @escaping (Base.Element) async throws -> Transformed + ) { + self.base = base.makeAsyncIterator() + self.option = option + self.transform = transform + } + + @inlinable + mutating func tryNext() async throws -> Base.Element? { + + switch option { + + case .depthFirst: + + while self.mapped_iterator != nil { + + if let element = try await self.mapped_iterator!.next() { + mapped.append(self.mapped_iterator!) + self.mapped_iterator = try await transform(element).makeAsyncIterator() + return element + } + + self.mapped_iterator = mapped.popLast() + } + + if self.base != nil { + + if let element = try await self.base!.next() { + self.mapped_iterator = try await transform(element).makeAsyncIterator() + return element + } + + self.base = nil + } + + return nil + + case .breadthFirst: + + if self.base != nil { + + if let element = try await self.base!.next() { + try await mapped.append(transform(element).makeAsyncIterator()) + return element + } + + self.base = nil + self.mapped_iterator = mapped.popFirst() + } + + while self.mapped_iterator != nil { + + if let element = try await self.mapped_iterator!.next() { + try await mapped.append(transform(element).makeAsyncIterator()) + return element + } + + self.mapped_iterator = mapped.popFirst() + } + + return nil + } + } + + @inlinable + public mutating func next() async throws -> Base.Element? { + + do { + + return try await self.tryNext() + + } catch { + + // set all state to empty + base = nil + mapped = [] + mapped_iterator = nil + + throw error + } + } + } +} + +extension AsyncThrowingRecursiveMapSequence: Sendable +where Base: Sendable, Base.Element: Sendable, Transformed: Sendable { } + +extension AsyncThrowingRecursiveMapSequence.AsyncIterator: Sendable +where Base.AsyncIterator: Sendable, Base.Element: Sendable, Transformed: Sendable, Transformed.AsyncIterator: Sendable { } diff --git a/Tests/AsyncAlgorithmsTests/TestRecursiveMap.swift b/Tests/AsyncAlgorithmsTests/TestRecursiveMap.swift new file mode 100644 index 00000000..01f57f4f --- /dev/null +++ b/Tests/AsyncAlgorithmsTests/TestRecursiveMap.swift @@ -0,0 +1,400 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +@preconcurrency import XCTest +import AsyncAlgorithms + +final class TestRecursiveMap: XCTestCase { + + struct Dir: Hashable { + + var id: UUID = UUID() + + var parent: UUID? + + var name: String + + } + + struct Path: Hashable { + + var id: UUID + + var path: String + + } + + func testAsyncRecursiveMap() async { + + var list: [Dir] = [] + list.append(Dir(name: "root")) + list.append(Dir(parent: list[0].id, name: "images")) + list.append(Dir(parent: list[0].id, name: "Users")) + list.append(Dir(parent: list[2].id, name: "Susan")) + list.append(Dir(parent: list[3].id, name: "Desktop")) + list.append(Dir(parent: list[1].id, name: "test.jpg")) + + let answer = [ + Path(id: list[0].id, path: "/root"), + Path(id: list[1].id, path: "/root/images"), + Path(id: list[2].id, path: "/root/Users"), + Path(id: list[5].id, path: "/root/images/test.jpg"), + Path(id: list[3].id, path: "/root/Users/Susan"), + Path(id: list[4].id, path: "/root/Users/Susan/Desktop"), + ] + + let _list = list + + let _result: AsyncRecursiveMapSequence = list.async + .compactMap { $0.parent == nil ? Path(id: $0.id, path: "/\($0.name)") : nil } + .recursiveMap(option: .breadthFirst) { parent in _list.async.compactMap { $0.parent == parent.id ? Path(id: $0.id, path: "\(parent.path)/\($0.name)") : nil } } + + var result: [Path] = [] + + for await item in _result { + result.append(item) + } + + XCTAssertEqual(result, answer) + } + + func testAsyncThrowingRecursiveMap() async throws { + + var list: [Dir] = [] + list.append(Dir(name: "root")) + list.append(Dir(parent: list[0].id, name: "images")) + list.append(Dir(parent: list[0].id, name: "Users")) + list.append(Dir(parent: list[2].id, name: "Susan")) + list.append(Dir(parent: list[3].id, name: "Desktop")) + list.append(Dir(parent: list[1].id, name: "test.jpg")) + + let answer = [ + Path(id: list[0].id, path: "/root"), + Path(id: list[1].id, path: "/root/images"), + Path(id: list[2].id, path: "/root/Users"), + Path(id: list[5].id, path: "/root/images/test.jpg"), + Path(id: list[3].id, path: "/root/Users/Susan"), + Path(id: list[4].id, path: "/root/Users/Susan/Desktop"), + ] + + let _list = list + + let _result: AsyncThrowingRecursiveMapSequence = list.async + .compactMap { $0.parent == nil ? Path(id: $0.id, path: "/\($0.name)") : nil } + .recursiveMap(option: .breadthFirst) { parent in _list.async.compactMap { $0.parent == parent.id ? Path(id: $0.id, path: "\(parent.path)/\($0.name)") : nil } } + + var result: [Path] = [] + + for try await item in _result { + result.append(item) + } + + XCTAssertEqual(result, answer) + } + + struct Node { + + var id: Int + + var children: [Node] = [] + } + + func testAsyncRecursiveMap2() async { + + let tree = [ + Node(id: 1, children: [ + Node(id: 2), + Node(id: 3, children: [ + Node(id: 4), + ]), + Node(id: 5), + ]), + Node(id: 6), + ] + + let nodes: AsyncRecursiveMapSequence = tree.async.recursiveMap { $0.children.async } // default depthFirst option + + var result: [Int] = [] + + for await node in nodes { + result.append(node.id) + } + + XCTAssertEqual(result, Array(1...6)) + } + + func testAsyncThrowingRecursiveMap2() async throws { + + let tree = [ + Node(id: 1, children: [ + Node(id: 2), + Node(id: 3, children: [ + Node(id: 4), + ]), + Node(id: 5), + ]), + Node(id: 6), + ] + + let nodes: AsyncThrowingRecursiveMapSequence = tree.async.recursiveMap { $0.children.async } // default depthFirst option + + var result: [Int] = [] + + for try await node in nodes { + result.append(node.id) + } + + XCTAssertEqual(result, Array(1...6)) + } + + func testAsyncRecursiveMap3() async { + + let tree = [ + Node(id: 1, children: [ + Node(id: 3), + Node(id: 4, children: [ + Node(id: 6), + ]), + Node(id: 5), + ]), + Node(id: 2), + ] + + let nodes: AsyncRecursiveMapSequence = tree.async.recursiveMap(option: .breadthFirst) { $0.children.async } + + var result: [Int] = [] + + for await node in nodes { + result.append(node.id) + } + + XCTAssertEqual(result, Array(1...6)) + } + + func testAsyncThrowingRecursiveMap3() async throws { + + let tree = [ + Node(id: 1, children: [ + Node(id: 3), + Node(id: 4, children: [ + Node(id: 6), + ]), + Node(id: 5), + ]), + Node(id: 2), + ] + + let nodes: AsyncThrowingRecursiveMapSequence = tree.async.recursiveMap(option: .breadthFirst) { $0.children.async } + + var result: [Int] = [] + + for try await node in nodes { + result.append(node.id) + } + + XCTAssertEqual(result, Array(1...6)) + } + + func testAsyncRecursiveMapWithBaseSequenceThrows() async throws { + + let tree = [ + Node(id: 1, children: [ + Node(id: 3), + Node(id: 4, children: [ + Node(id: 6), + ]), + Node(id: 5), + ]), + Node(id: 2), + ] + + let throwing_base = tree.async.map { node async throws -> Node in + if node.id == 2 { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } + return node + } + + let nodes: AsyncRecursiveMapSequence = throwing_base.recursiveMap { $0.children.async } + + var result: [Int] = [] + var iterator = nodes.makeAsyncIterator() + + do { + + while let node = try await iterator.next() { + result.append(node.id) + } + + XCTFail() + + } catch { + + XCTAssertEqual((error as NSError).code, -1) // we got throw from the closure + } + + let expectedNil = try await iterator.next() // we should get nil in here + XCTAssertNil(expectedNil) + } + + func testAsyncThrowingRecursiveMapWithBaseSequenceThrows() async throws { + + let tree = [ + Node(id: 1, children: [ + Node(id: 3), + Node(id: 4, children: [ + Node(id: 6), + ]), + Node(id: 5), + ]), + Node(id: 2), + ] + + let throwing_base = tree.async.map { node async throws -> Node in + if node.id == 2 { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } + return node + } + + let nodes: AsyncThrowingRecursiveMapSequence = throwing_base.recursiveMap { $0.children.async } + + var result: [Int] = [] + var iterator = nodes.makeAsyncIterator() + + do { + + while let node = try await iterator.next() { + result.append(node.id) + } + + XCTFail() + + } catch { + + XCTAssertEqual((error as NSError).code, -1) // we got throw from the closure + } + + let expectedNil = try await iterator.next() // we should get nil in here + XCTAssertNil(expectedNil) + } + + func testAsyncRecursiveMapWithMappedSequenceThrows() async throws { + + let tree = [ + Node(id: 1, children: [ + Node(id: 3), + Node(id: 4, children: [ + Node(id: 6), + ]), + Node(id: 5), + ]), + Node(id: 2), + ] + + let nodes: AsyncRecursiveMapSequence = tree.async.recursiveMap { $0.children.async.map { node async throws -> Node in + if node.id == 4 { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } + return node + } } + + var result: [Int] = [] + var iterator = nodes.makeAsyncIterator() + + do { + + while let node = try await iterator.next() { + result.append(node.id) + } + + XCTFail() + + } catch { + + XCTAssertEqual((error as NSError).code, -1) // we got throw from the closure + } + + let expectedNil = try await iterator.next() // we should get nil in here + XCTAssertNil(expectedNil) + } + + func testAsyncThrowingRecursiveMapWithMappedSequenceThrows() async throws { + + let tree = [ + Node(id: 1, children: [ + Node(id: 3), + Node(id: 4, children: [ + Node(id: 6), + ]), + Node(id: 5), + ]), + Node(id: 2), + ] + + let nodes: AsyncThrowingRecursiveMapSequence = tree.async.recursiveMap { $0.children.async.map { node async throws -> Node in + if node.id == 4 { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } + return node + } } + + var result: [Int] = [] + var iterator = nodes.makeAsyncIterator() + + do { + + while let node = try await iterator.next() { + result.append(node.id) + } + + XCTFail() + + } catch { + + XCTAssertEqual((error as NSError).code, -1) // we got throw from the closure + } + + let expectedNil = try await iterator.next() // we should get nil in here + XCTAssertNil(expectedNil) + } + + func testAsyncThrowingRecursiveMapWithClosureThrows() async throws { + + let tree = [ + Node(id: 1, children: [ + Node(id: 3), + Node(id: 4, children: [ + Node(id: 6), + ]), + Node(id: 5), + ]), + Node(id: 2), + ] + + let nodes = tree.async.recursiveMap { node async throws -> AsyncLazySequence<[TestRecursiveMap.Node]> in + if node.id == 4 { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } + return node.children.async + } + + var result: [Int] = [] + var iterator = nodes.makeAsyncIterator() + + do { + + while let node = try await iterator.next() { + result.append(node.id) + } + + XCTFail() + + } catch { + + XCTAssertEqual((error as NSError).code, -1) // we got throw from the closure + } + + let expectedNil = try await iterator.next() // we should get nil in here + XCTAssertNil(expectedNil) + } + +}