-
Notifications
You must be signed in to change notification settings - Fork 155
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
base: main
Are you sure you want to change the base?
Changes from 6 commits
27f397a
79b0aae
84cccba
eaff804
38199a0
6774fc6
5d63ffe
de6e51b
e01a2ae
a9fffdd
db8f025
4aebd4c
80cd630
edeeffd
8588e71
c7e313e
7f0afd0
2f398cf
5135e3d
fd31319
179c924
864bd80
9f43c56
8ecc2fb
25f855f
a669b68
2e05d91
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> | ||
} | ||
``` | ||
|
||
### Complexity | ||
|
||
Calling this method is O(_1_). |
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> = [] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
||
@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? { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { } |
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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { } |
There was a problem hiding this comment.
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.