I’m going through the list of common sequence APIs that I’ve had to implement multiple times in several projects and I’d like to pitch a couple of APIs on sequence, modulo naming.
/// Presents a sequence of the elements in `firstSequence` followed by those in `secondSequence`.
/// An infinite sequence of containing the elements of `sequence` repeated forever.
/// An sequence containing the elements of `sequence` repeated twice.
/// The elements of `sequence` batched by subsequences of `count == 2`, except for remainder elements, which an have a `count < 2`.
You’ve probably seen some of these in Python’s itertools. These are the ones that I find come up the most often.
I have implementations for these that I can share if you’d like to try it out but they’re pretty simple to implement; I’m sure a lot of you have implemented them yourselves.
Would love to have these additions built in! I recreate them too when I need them. The repeated one has come up a couple times before, plus batched in a sideways sort of way. Linking those threads for reference:
I would say the main benefit of this particular signature is that the first and second sequence can be completely different concrete sequences so long as they have the same element, like this:
would work as well as:
So, for a variadic/multi-sequence signature, in order to maximize its benefit, we would want to be able to pass any group of sequences together so long as their Element is the same. The only way I know to do that today is to involve AnySequence in some way:
first.followed(by: AnySequence(second), AnySequence(third))
// or, perhaps something using `AnySequence` and `reduce`
As you’ve described, the most ergonomic solution won’t be available until we have variadic generics. At the moment, the only workaround to the AnySequence problem you describe is to provide multiple implementations of followed(by:) for taking different quantities of sequences to chain, i.e. followed(by:), followed(by:_:), followed(by:_:_:), etc.
While functional, this is pretty ugly—I think the single followed(by:) is worth proposing now, and the variadic version is worth pursuing once Swift can support it more elegantly.
It’s also worth noting that followed(by:) could be made variadic over a single OtherSequence where OtherSequence.Element == Element, which would (for example) allow a Set<Int> to be chained with several Array<Int> in a single call—though this may introduce confusion for users who do not understand why multiple different sequence types cannot be used.
Hm, I think you’re thinking of extension Sequence where Element: Sequence. I don’t think that’s quite the same thing and I don’t think it’s great API, not a very discoverable conditional extension. For the pattern [x, y].joined(), x and y need to be the same concrete type, so it’s back to the same variadic/AnySequence problem as before.
Another downside to the [seq1, seq2, seq3].joined() syntax is that it requires heap allocation of an array—I imagine that wouldn’t get optimized away. That ought to be completely unnecessary to lazily concatenate existing sequences.
Gotcha! Yep, the sketch I made a while back used a struct of the two, effectively your Pair<T>, although it was a little more specific to the chaining. I will try to dig up a gist so you all can get a feel for the ergonomics.
I think you’re absolutely right that the primary use is for joining just one with another. If we accept a variadic list, then it’s not far off from writing:
// if they're the same type:
[first, second, third].joined()
// if they're different types:
[AnySequence(first), AnySequence(second), AnySequence(third)].joined()
(edit: I apparently missed that several other people made this point.)
The big advantage of adding a custom method and type for joining exactly two sequences/collections is that it can model the attributes of the underlying types. For example, if two ranges are joined, the result can still be a RandomAccessCollection. There’s a Concatenation type in the Swift test suite that fills that role.
That’s an unfortunate design choice, then, that won’t make sense to many mathematically inclined readers.
But I’ll play along. What’s the result of Set([1,3,2]).followed(by: [5,3,0])? [1,3,2,5,0] or [1,2,5,3,0]? Or [0,1,2,3,5]? Imho, the only sanity-preserving answer is, “order doesn’t matter for sets” – yup, : Sequence doesn’t make sense.