General Feedback

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.

Bad:

  • As far as i can tell: nothing. Great work!
1 Like

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.

I personally prefer doing this:

let userChannel = AsyncChannel<User>()

// ...
try await userStream1
    .debounce(for: .seconds(0.5))
    .zip(with: userStream2)
    .merge(with: userChannel)
    .map(\.userName)
    .forEach { db.add($0) }

Over this:

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.

1 Like

The reactive form is strongly my preference syntactically as well, I just feel dirty using for loops now :)

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.

I'm thinking of making a proposal about offering operator-style alternatives to the global functions like zip, merge, and co. What do you think?

Not terribly difficult to do. I'd certainly use them.

I think @jmjauer meant that he's not sure if offering global functions offers much of an advantage over using the structs themselves. For example:

let merged = merge(stream1, stream2)
// vs
let merged = Merge2(stream1, stream2) // right now it's called AsyncMerge2Sequence

I tend to think that global functions tend to feel more natural than structs, especially with the prior art of zip in the Standard Library.

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.

1 Like

Yes exactly

I agree, symmetry to the sync sequence api is a good thing

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.

This is what I also prefer. Using these as operators makes it less clear what the semantics are imo.

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".

1 Like

This is highly subjective, but I prefer the syntactical symmetry since both input sequences are equal in their importance.

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().

2 Likes

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.

2 Likes