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

Add recursiveMap(_:) methods #118

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
27f397a
add recursiveMap methods
SusanDoggie Mar 27, 2022
79b0aae
Create TestRecursiveMap.swift
SusanDoggie Mar 27, 2022
84cccba
add document
SusanDoggie Mar 28, 2022
eaff804
Update README.md
SusanDoggie Mar 28, 2022
38199a0
rename
SusanDoggie Mar 28, 2022
6774fc6
Update Effects.md
SusanDoggie Mar 28, 2022
5d63ffe
add TraversalOption
SusanDoggie Mar 28, 2022
de6e51b
Update TestRecursiveMap.swift
SusanDoggie Mar 28, 2022
e01a2ae
immutable
SusanDoggie Mar 28, 2022
a9fffdd
add closure throws test
SusanDoggie Mar 28, 2022
db8f025
documentation
SusanDoggie Mar 28, 2022
4aebd4c
fix the missing Sendable
SusanDoggie Mar 28, 2022
80cd630
Update RecursiveMap.md
SusanDoggie Mar 28, 2022
edeeffd
Update RecursiveMap.md
SusanDoggie Mar 28, 2022
8588e71
Update RecursiveMap.md
SusanDoggie Mar 28, 2022
c7e313e
Update RecursiveMap.md
SusanDoggie Mar 28, 2022
7f0afd0
Update RecursiveMap.md
SusanDoggie Mar 28, 2022
2f398cf
Update RecursiveMap.md
SusanDoggie Mar 28, 2022
5135e3d
test all route of throws
SusanDoggie Mar 28, 2022
fd31319
Merge branch 'patch-1' of https://github.com/SusanDoggie/swift-async-…
SusanDoggie Mar 28, 2022
179c924
Merge branch 'main'
SusanDoggie Apr 19, 2022
864bd80
move file
SusanDoggie Apr 19, 2022
9f43c56
Create NNN-recursiveMap.md
SusanDoggie Apr 19, 2022
8ecc2fb
Update NNN-recursiveMap.md
SusanDoggie Apr 19, 2022
25f855f
Update NNN-recursiveMap.md
SusanDoggie Apr 19, 2022
a669b68
Update NNN-recursiveMap.md
SusanDoggie Apr 19, 2022
2e05d91
Update NNN-recursiveMap.md
SusanDoggie Apr 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Guides/Effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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 |
Expand All @@ -50,6 +52,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 |
Expand Down
56 changes: 56 additions & 0 deletions Guides/RecursiveMap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 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)
]

Produces a sequence containing the original sequence followed by recursive mapped sequence.

```swift
struct Node {
var id: Int
var children: [Node] = []
}
let tree = [
Node(id: 1, children: [
Node(id: 3),
Node(id: 4, children: [
Node(id: 6),
]),
Node(id: 5),
]),
Node(id: 2),
]
for await node in tree.async.recursiveMap({ $0.children.async }) {
print(node.id)
}
// 1
// 2
// 3
// 4
// 5
// 6
```

## Detailed Design

The `recursiveMap(_:)` method is declared as `AsyncSequence` extensions, and return `AsyncRecursiveMapSequence` or `AsyncThrowingRecursiveMapSequence` instance:

```swift
extension AsyncSequence {
public func recursiveMap<S>(
_ transform: @Sendable @escaping (Element) async -> S
) -> AsyncRecursiveMapSequence<Self, S>

public func recursiveMap<S>(
_ transform: @Sendable @escaping (Element) async throws -> S
) -> AsyncThrowingRecursiveMapSequence<Self, S>
}
```

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be good here to give a bit more detail, particularly things that are helpful: how does it work with cancellation, identifying the Sensibility requirements, how do they interact with rethrows etc is good to record in the guides.

Also links to existing counterparts etc.

### Complexity

Calling this method is O(_1_).
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ This package is the home for these APIs. Development and API design take place o
#### Other useful asynchronous sequences
- [`chunks(...)` and `chunked(...)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Chunked.md): Collect values into chunks.
- [`compacted()`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Compacted.md): Remove nil values from an asynchronous sequence.
- [`recursiveMap(_:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/RecursiveMap.md): Produces a sequence containing the original sequence followed by recursive mapped sequence.
- [`removeDuplicates()`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/RemoveDuplicates.md): Remove sequentially adjacent duplicate values.
- [`interspersed(with:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Intersperse.md): Place a value between every two elements of an asynchronous sequence.

Expand Down
127 changes: 127 additions & 0 deletions Sources/AsyncAlgorithms/AsyncRecursiveMapSequence.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//===----------------------------------------------------------------------===//
//
// 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 followed by recursive mapped sequence.
///
/// ```
/// struct Node {
/// var id: Int
/// var children: [Node] = []
/// }
/// let tree = [
/// Node(id: 1, children: [
/// Node(id: 3),
/// Node(id: 4, children: [
/// Node(id: 6),
/// ]),
/// Node(id: 5),
/// ]),
/// Node(id: 2),
/// ]
/// for await node in tree.async.recursiveMap({ $0.children.async }) {
/// print(node.id)
/// }
/// // 1
/// // 2
/// // 3
/// // 4
/// // 5
/// // 6
/// ```
///
/// - Parameters:
/// - 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<C>(_ transform: @Sendable @escaping (Element) async -> C) -> AsyncRecursiveMapSequence<Self, C> {
return AsyncRecursiveMapSequence(self, transform)
}
}

public struct AsyncRecursiveMapSequence<Base: AsyncSequence, Transformed: AsyncSequence>: AsyncSequence where Base.Element == Transformed.Element {

public typealias Element = Base.Element

@usableFromInline
let base: Base

@usableFromInline
let transform: @Sendable (Base.Element) async -> Transformed

@inlinable
init(_ base: Base, _ transform: @Sendable @escaping (Base.Element) async -> Transformed) {
self.base = base
self.transform = transform
}

@inlinable
public func makeAsyncIterator() -> AsyncIterator {
return AsyncIterator(base, transform)
}
}

extension AsyncRecursiveMapSequence {

public struct AsyncIterator: AsyncIteratorProtocol {

@usableFromInline
var base: Base.AsyncIterator?

@usableFromInline
var mapped: ArraySlice<Transformed> = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so what happens if the base is indefinite? does this potentially grow with out bound?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it’s definitely problems. Maybe I should change to depth first algorithm and it shouldn’t have indefinite depth of tree hopefully.

Copy link
Contributor

@kingreza kingreza Mar 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty cool!

I have a somewhat and open-ended question, I believe using depth first would change the expected output in the example above from 1,2,3,4,5,6 to 1,3,4,6,5,2. The traversal would still be correct but given that the output varies so widely depending on the type of traversal, should that be communicated in the function name/signature? Or is order not that important in this scenario?

Is the utility of this function its ability to visit each node once and iterate over the tree or do/should we care about the order in the sequence?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is a simplified version of SQL recursive CTE, I implemented breadth-first traversal because the form of the algorithm is already in my head.
It can be done by function name, or using a enum option for choosing the traversal methods.


@usableFromInline
var mapped_iterator: Transformed.AsyncIterator?

@usableFromInline
var transform: @Sendable (Base.Element) async -> Transformed

@inlinable
init(_ base: Base, _ transform: @Sendable @escaping (Base.Element) async -> Transformed) {
self.base = base.makeAsyncIterator()
self.transform = transform
}

@inlinable
public mutating func next() async rethrows -> Base.Element? {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems to be breadth first recursion; would it perhaps make more sense in the async case to be depth first?


if self.base != nil {

if let element = try await self.base?.next() {
await mapped.append(transform(element))
return element
}

self.base = nil
self.mapped_iterator = mapped.popFirst()?.makeAsyncIterator()
}

while self.mapped_iterator != nil {

if let element = try await self.mapped_iterator?.next() {
await mapped.append(transform(element))
return element
}

self.mapped_iterator = mapped.popFirst()?.makeAsyncIterator()
}

return nil
}
}
}

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 { }
127 changes: 127 additions & 0 deletions Sources/AsyncAlgorithms/AsyncThrowingRecursiveMapSequence.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//===----------------------------------------------------------------------===//
//
// 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 followed by recursive mapped sequence.
///
/// ```
/// struct Node {
/// var id: Int
/// var children: [Node] = []
/// }
/// let tree = [
/// Node(id: 1, children: [
/// Node(id: 3),
/// Node(id: 4, children: [
/// Node(id: 6),
/// ]),
/// Node(id: 5),
/// ]),
/// Node(id: 2),
/// ]
/// for await node in tree.async.recursiveMap({ $0.children.async }) {
/// print(node.id)
/// }
/// // 1
/// // 2
/// // 3
/// // 4
/// // 5
/// // 6
/// ```
///
/// - Parameters:
/// - 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<C>(_ transform: @Sendable @escaping (Element) async throws -> C) -> AsyncThrowingRecursiveMapSequence<Self, C> {
return AsyncThrowingRecursiveMapSequence(self, transform)
}
}

public struct AsyncThrowingRecursiveMapSequence<Base: AsyncSequence, Transformed: AsyncSequence>: AsyncSequence where Base.Element == Transformed.Element {

public typealias Element = Base.Element

@usableFromInline
let base: Base

@usableFromInline
let transform: @Sendable (Base.Element) async throws -> Transformed

@inlinable
init(_ base: Base, _ transform: @Sendable @escaping (Base.Element) async throws -> Transformed) {
self.base = base
self.transform = transform
}

@inlinable
public func makeAsyncIterator() -> AsyncIterator {
return AsyncIterator(base, transform)
}
}

extension AsyncThrowingRecursiveMapSequence {

public struct AsyncIterator: AsyncIteratorProtocol {

@usableFromInline
var base: Base.AsyncIterator?

@usableFromInline
var mapped: ArraySlice<Transformed> = []

@usableFromInline
var mapped_iterator: Transformed.AsyncIterator?

@usableFromInline
var transform: @Sendable (Base.Element) async throws -> Transformed

@inlinable
init(_ base: Base, _ transform: @Sendable @escaping (Base.Element) async throws -> Transformed) {
self.base = base.makeAsyncIterator()
self.transform = transform
}

@inlinable
public mutating func next() async throws -> Base.Element? {

if self.base != nil {

if let element = try await self.base?.next() {
try await mapped.append(transform(element))
return element
}

self.base = nil
self.mapped_iterator = mapped.popFirst()?.makeAsyncIterator()
}

while self.mapped_iterator != nil {

if let element = try await self.mapped_iterator?.next() {
try await mapped.append(transform(element))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this throws, don't you have the iterator potentially be in a bad state? where it may throw an error for this call to next but then the next call to next violates the expectation that past a nil or a thrown error the iterators must return nil for subsequent calls to next.

return element
}

self.mapped_iterator = mapped.popFirst()?.makeAsyncIterator()
}

return nil
}
}
}

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 { }
Loading