We could use this thread for general feedback about the package. My 2 cents:
Good:
An official package from Apple is a lot better than ad hoc solutions for each project (which happens very often right now).
Each AsyncSequence implementation of the package seems to be very focused and only solves one specific use case.
While some parts are still missing, the choosen set of AsyncSequences is very promising.
Neutral:
I am not sure if the global functions like merge or zip are of much use. Basically they map a global identifier to another global identifier. The only advantage I see is that they avoid the number of arguments in their names.
I see some of sequence check the cancellation of Task and some of don't. is it good practice for checking cancellation and stop enumerating the next value?
I wish we were also offered operator-like alternatives to the global functions like merge and zip, so that I can keep the reactive-streams style operator-chaining in my code.
let userChannel = AsyncChannel<User>()
// ...
let zippedUsers = zip(userStream1.debounce(for: .seconds(0.5)), userStream2)
let mergedUsers = merge(zippedUsers, userChannel)
let finalSeq = mergedUsers.map(\.userName)
for try await userName in finalSeq {
db.add(userName)
}
I understand it's a purely stylistic preference, but it would definitely be nice to have the choice between the two.
I find them incredibly useful. For my work flow the std lib map/flatMap etc operators were largely useless until these operators and subject-like operators are available.
The ones that check for cancellation are the ones that may need to respond to cancellation via a task. Otherwise cancellation is directly handled by their base sequences.
The rule of thumb is that they should be at least as responsive as the bases constructing them, and if they run sub-tasks to produce values then those subtasks should be cancelled with a handler if possible in a timely manner.
The one exception to all of these rules is the AsyncBufferedByteIterator; which does throw on cancellation to disambiguate end of sequence.
zip(_:_:) was chosen because it mimics the zip function from the standard library. Additionally it holds no real preference on which side happens first - so there is not really any "primary" part of the zip.
I think @Ben_Cohen expressed some strong opinions about that earlier when we were first iterating on AsyncSequence in the _Concurrency library.
Couldn't the same be said about Sequence's elementsEqual(_:by:)? Similarly to zip, there isn't really a primary sequence – we're just comparing two sequences. Or is there some inherent difference between elementsEqual and zip that I'm missing out? To me it seems like there is precedent to adding methods to a type even if their isn't really a clear "primary" and "secondary" part, for ergonomic's sake. And I think that applies just as much to zip, merge, and co.
I'm interested in understanding how much the semantics differ between the two for you. To me, the two read quite similarly:
zip(stream1, stream2) # Zip stream1 and stream2 together
stream1.zip(with: stream2) # Zip stream1 with stream2
Personally, these semantically mean the same to me. The image of zipping the two streams together like a zip fastener appears just as strongly in my head with the first one as with the second one. What do you think?
I think that particular one has to do with SE-0203 where the order of the first sequence is the primary comparator. @xwu might be able to comment more on the intent there.
The first has a nice symmetry with existing stdlib stuff, but also has symmetry with the output; the sequence produced is the same tuple construction as the arguments passed in; (Base1, Base2) -> (Base1.Element, Base2.Element) whereas the second takes a bit of extra swift knowledge about unapplied functions to come to the same conclusion. Don't get me wrong, they are pretty much isomorphic from a functional standpoint, just coming from a welcoming to either functional or imperative as a first principle of learning feels like it is better to be consistent than to be "right".
I see what you mean. It's true that a global zip function is locally consistent, however I'm not sure how consistent it is with the rest of the library's API design, with most of its algorithms being defined as methods.
I think @jmjauer is right, this is highly subjective, and more of a stylistic preference than anything. IMO a library leaves us more flexibility to offer alternatives to cater to more styles (similarly to the String Processing push with both syntactic Regex and a Regex DSL). Would you like me to maybe create a new thread and draft up a proposal to see what the community thinks?
IMO, the library should vend both global and instance methods. The ergonomics of zip and similar globals, as has been discussed in past and in the swift-algorithms library, is rather poor when composed with other operations.
I'd also like to see instance version of the various collection initializers, allowing us to create .array() and .dictionary().
I’d actually be pretty disappointed if we resorted to such manual duplication of APIs; rather, as I’ve said before, I think what the (very real) ergonomics issue calls for is a general solution to make global functions and initializers easily chainable (some sort of “pipe” operator, for example).
That sounds plausible, but I can’t recall anything explicitly for certain about it. elementsEqual is rather an esoteric API though, and it has other significant deficiencies which weren’t deemed to be worth correcting, so I don’t think it should be considered a reference on which to model other APIs.