-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Provide doc comments for AsyncStream/AsyncThrowingStream #38535
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
Provide doc comments for AsyncStream/AsyncThrowingStream #38535
Conversation
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.
Looks good - minor comments throughout. I read through AsyncStream.swift
in detail and it looks like the API in AsyngThrowingStream.swift
is largely similar. If there are symbols I missed by skimming the latter, please call them out.
/// Use this convenience initializer when you have an asychronous function | ||
/// that can produce elements for the stream, and don't want to invoke | ||
/// a continuation manually. This initializer "unfolds" your closure into | ||
/// a full-blown asynchronous stream. The created stream handles adherence to |
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.
This is the first time I've seen "adherence" to a protocol. What the sentence is describing isn't quite protocol conformance, which is the usual term, but seems a bit more general.
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.
This is kind of borrowing from the swift-evolution proposal:
This affords the case in which that unfolded function can leave the specification adherence of AsyncSequence to AsyncStream. In short the
init(unfolding:onCancel:)
initializer handles the terminal cases as well as the cancellation.
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.
Conformance might be better? Since it is a behavioral requirement of AsyncSequence (not a compile time enforced property)?
Or perhaps qualify it as "well behaved conformance"?
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.
I think we can just call it "conformance".
By analogy, supposed I made a type that implemented the methods that Collection
requires, but crashed if you tried to iterate more than 3 times. That type doesn't correctly conform to the Collection
protocol because a collection must support multiple iteration. Many protocols have requirements that can't be expressed in code like this.
Except in the context of discussing this sort of incorrect conformance, we shouldn't need to distinguish well-behaved conformance.
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.
I think we can just call it "conformance".
By analogy, supposed I made a type that implemented the methods that
Collection
requires, but crashed if you tried to iterate more than 3 times. That type doesn't correctly conform to theCollection
protocol because a collection must support multiple iteration. Many protocols have requirements that can't be expressed in code like this.Except in the context of discussing this sort of incorrect conformance, we shouldn't need to distinguish well-behaved conformance.
@natecook1000 what do you think?
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.
Personally, I'm fine with "conformance". I kept "adherence" as a synonym borrowed from the API proposal, because AsyncStream
conforms to AsyncSequence
in the first place, so it's almost a tautology to talk about conformance at all. I think the real point here is that the developer sends in a closure and this initializer provides all the other piping to serve as an AsyncStream
, which in turn is an AsyncSequence
. And that's what the final sentence here is about.
/// let stream = AsyncStream<Int>( | ||
/// unfolding: { | ||
/// await Task.sleep(1 * 1_000_000_000) | ||
/// return Int.random(in: 1...10) | ||
/// }, | ||
/// onCancel: { @Sendable () in print ("Canceled.") } | ||
/// ) |
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.
Can/should this use the multiple trailing closure syntax from Swift 5.3?
Co-authored-by: Alex Martini <[email protected]>
Co-authored-by: Joseph Heck <[email protected]>
Co-authored-by: Harlan Haskins <[email protected]>
/// yielding. | ||
/// | ||
/// > Note: Acting on the remaining count is valid only when calls to | ||
/// yield are mutually exclusive. |
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.
The info here is more than that - the remaining is a hint to how much may be remaining, a subsequent call may race on the potentials of consuming values so the remaining is technically an underestimate (from a thread safety standpoint)
case terminated | ||
} | ||
|
||
/// A strategy that handles exhaustion of a buffer’s capacity. | ||
public enum BufferingPolicy { | ||
/// Continue to add to the buffer, treating its capacity as infinite. |
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 to point out as a reiteration that this is just a no limit buffer, in that it does not allocate unlimited ram ;) or something more eloquent than that...
case unbounded | ||
|
||
/// When the buffer is full, discard the newly received element. | ||
/// This enforces keeping the specified amount of oldest values. | ||
/// | ||
/// This strategy enforces keeping the specified number of oldest values. |
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.
might want to add an "at most" to qualify the "specified number"
case bufferingOldest(Int) | ||
|
||
/// When the buffer is full, discard the oldest element in the buffer. | ||
/// This enforces keeping the specified amount of newest values. | ||
/// | ||
/// This strategy enforces keeping the specified number of newest values. |
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.
ditto here on the "at most"
/// needed cleanup in the cancellation handler. After reaching a terminal | ||
/// state, the `AsyncStream` disposes of the callback. |
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.
the closure internally is set to nil, removing any references it has to it.
/// | ||
/// - Parameter elementType: The type of element the `AsyncStream` | ||
/// produces. | ||
/// - Parameter limit: The maximum number of elements to hold in the buffer. |
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 is no longer called limit, it is now bufferingPolicy
/// | ||
/// let stream = AsyncStream<Int>(Int.self, | ||
/// bufferingPolicy: .bufferingNewest(5)) { continuation in | ||
/// Task.detached { |
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.
Im not sure how keepRunning can be safe (at least easily...) and likely that should be set to false in the onTermination
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.
Would you recommend a while true
here, instead? That would prevent us from ever reaching continuation.finish()
. Maybe it would be better pedagogy to change the stream to use a for
-in
loop to produce 100 random numbers, one per second, and then finish.
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.
@phausler Changed this to a for
-in
loop. Does this look OK now?
/// Use this convenience initializer when you have an asychronous function | ||
/// that can produce elements for the stream, and don't want to invoke | ||
/// a continuation manually. This initializer "unfolds" your closure into | ||
/// a full-blown asynchronous stream. The created stream handles adherence to |
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.
Conformance might be better? Since it is a behavioral requirement of AsyncSequence (not a compile time enforced property)?
Or perhaps qualify it as "well behaved conformance"?
/// | ||
/// This can be called more than once and returns to the caller immediately | ||
/// without blocking for any awaiting consumption from the iteration. | ||
/// without blocking for any awaiting consuption from the iteration. |
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.
consumption meaning the consuming of values. this is a spelling mistake?
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.
Edits and comments throughout. I think there's one FIXME in the first file (AsyncStream).
/// property, return an `AsyncStream`, whose `build` closure -- called at | ||
/// runtime to create the stream -- does the following: | ||
/// | ||
/// 1. Creates a `QuakeMonitor` instance. |
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.
This should be a bulleted list, not a numbered list.
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.
I thought we preferred numbered lists for steps performed in sequence?
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.
The preceding text isn't clear that these happen in sequence. If that's the case, then maybe tweak the lead.
/// The closure you provide to the `AsyncStream` in | ||
/// `init(_:bufferingPolicy:_:)` receives an instance of this type when | ||
/// invoked. Use this continuation to provide elements to the stream by | ||
/// calling one of the `yield` methods, then terminate the stream normally by |
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.
/// calling one of the `yield` methods, then terminate the stream normally by | |
/// calling one of the `yield` methods, then terminate the stream by |
You don't need the adverb here.
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.
I kind of think we do; terminate "normally" means to end the stream without throwing an error. If we did throw an error here, we'd say "terminate the stream with an error".
Co-authored-by: Chuck Toporek <[email protected]>
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.
OK to go.
/// property, return an `AsyncStream`, whose `build` closure -- called at | ||
/// runtime to create the stream -- does the following: | ||
/// | ||
/// 1. Creates a `QuakeMonitor` instance. |
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.
The preceding text isn't clear that these happen in sequence. If that's the case, then maybe tweak the lead.
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.
Approved with changes. Some typo fixes and what looks like edits from the first pass that were skipped.
/// Use this convenience initializer when you have an asychronous function | ||
/// that can produce elements for the stream, and don't want to invoke | ||
/// a continuation manually. This initializer "unfolds" your closure into | ||
/// a full-blown asynchronous stream. The created stream handles adherence to |
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.
/// a full-blown asynchronous stream. The created stream handles adherence to | |
/// an asynchronous stream. The created stream handles adherence to |
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.
Don't use "full-blown" as it's jargon that can cause issues for localization.
Co-authored-by: Chuck Toporek <[email protected]>
@swift-ci Please test. |
Build failed |
@swift-ci Please test macOS |
Source documentation for AsyncStream, AsyncThrowingStream, and their related types.