Evolution of Collection Types in Swift
By: Valeriy Bragin
Tags: iOS, Xcode, Mobile, Programming, Swift

Sequence vs Collection — Some More Variations on a Theme

If you’re not yet overburdened with the above, let me share some more speculations on similarities and differences between the Sequence and Collection protocols. Hopefully you’ll have some practical takeaways even from the vague allegations in the section that follows.

Being different semantically, these protocols also provide operations of a somewhat different nature. This creates a certain division of roles between them among the APIs of each concrete collection type that conforms to both protocols. Feeling which protocol some method or property is more inherent to may be helpful when it comes to designing your own API that involves collections. Although in most real-life cases you are just okay with specifying a concrete type, sometimes you do have to be less restrictive and put Collection , or even less restrictive and use Sequence , depending on which behaviour or portion of their API is expected or necessary in this particular scope. After all, keeping your API as generic as possible is a good practice, and it would never harm if used wisely.

So the general rule of thumb, as you may have already guessed, would be that Collection is primarily used when you have some knowledge of certain elements’ positions or need to gain this knowledge for future use, whereas Sequence is just enough if you’re okay to flip through all of them from the beginning.

But here are some further reflections. Notice that the most typical operations over Sequence cover the cases where you deal with the multitude of its elements as a whole, like converting it all with map , digesting it all with reduce , boiling it down with filter , etc. Even when you search for an individual element of Sequence (e.g., with first(where:) ), internally it still tests all of them one by one, so it generally still interacts with them as a list.

To put it mathematically (just in case you also like making simple things complicated), such operations are effectively the functions in which input is a list of elements and output is also a list of elements. It’s just a particular case that the result may happen to be an empty list, or that it is forcibly trimmed to be just one element. For example, first(where:) , the primary means of accessing a certain element in the sequence, is just a trimmed version of filter(where:) . It drops all but the first element of the results of filtering or returns nil , if filtering resulted in an empty list. And, let’s say, contains(where:) is just another way of saying whether the size of the list of elements retrieved is more than zero.

By contrast, Collection is fundamentally aimed at giving an unambiguous and guaranteed result. The indexed subscript, the heart of the API of Collection , provides one and only one element. If there is no such single element, it crashes. So it is not optional, unlike most of the semantically corresponding methods of Sequence . This represents the mathematical idea of projection from a list of elements to a single and obligatory one. Again, this is not a strict rule or definition but more like trying to find some patterns and regularities.

Another distinction to be mentioned again is that only with Collection do you first get the ability to know the number of its elements. It’s not possible with Sequence because you never know beforehand when the iteration over it will reach the last element. For the same reason, be aware that Sequence is conceptually infinite, so both traversing it and using most of its methods (that internally still involve traversing) are not quite safe. The next() method of its iterator may keep returning a non-nil value till the end of time. This is not something you’re likely to run into when using sequences from a standard library, but when dealing with an unknown sequence, you can’t be 100% sure that you won’t get stuck inside of some method.

It looks like the only methods you can rely on are those dealing with a finite prefix (like dropFirst(Int) and prefix(Int) ). By the way, not knowing the size of a sequence also prevents you from accessing the last element and any logic that involves a suffix. (Well, frankly speaking, there are still suffix() and dropLast() methods in Sequence , but they require deliberate restriction of the receiver to be finite and are somewhat dissonant here theoretically).