Search code examples
swiftiteratorprotocolssequencecomparable

When to use Protocols like Sequence, Iterator, Collection in Swift


I was going thru swift tutorials and I come across examples implementing protocols like Sequence, sometimes Collection, sometimes Iterator

I looked when to really use this protocols but I never found one. All other materials says how to use it but not when to use it.

Can someone please advice where I can learn this. Any quick tip would really be helpful.

Thanks


Solution

  • Writing routines for specific types, such as Array, is often simplest, but writing them for the Sequence protocol, for example, offers greater flexibility. I most commonly come across this when writing extensions for collections, which, by definition, can be reused elsewhere and where flexibility is of the greatest value.

    Consider, for example, this Array extension that sums or prints the values:

    extension Array where Element == Int {
        func sum() -> Int {
            var sum = 0
            for element in self {
                sum += element
            }
            return sum
        }
    
        func printValues() {
            print("printing values")
            for element in self {
                print(element)
            }
        }
    }
    

    That obviously works if you really have an array, e.g.

    let array = [1, 2, 3, 4, 5, 6, 7, 8, 9]
    let total = array.sum()                 // fine
    array.printValues()                     // fine
    

    But that won’t work if you have a subrange, for example:

    let total = array[0...3].sum()          // this won’t work
    array[0...3].printValues()              // this won’t work
    

    Or if you have a set:

    let set: Set = [1, 2, 3]
    let total = set.sum()                   // this won’t work
    

    You would have to convert this sub array or that Set to array to work. However, rather than defining this as an Array extension, you can instead define your extension on Sequence:

    extension Collection where Element: AdditiveArithmetic {
        func sum() -> Element {
            var sum: Element = .zero
            for element in self {
                sum += element
            }
            return sum
        }
    }
    
    extension Sequence where Element: CustomStringConvertible {
        func printValues() {
            print("printing values")
            for element in self {
                print(element)
            }
        }
    }
    

    It looks pretty much the same, but these rendition will work for the appropriate types, e.g.:

    let array = [1, 2, 3, 4, 5, 6, 7, 8, 9]
    print(array[0..<3].sum())               // works for array slices
    array[0..<3].printValues()    
    (200..<300).printValues()               // or ranges
    

    In the above, I used Sequence/Collection where appropriate. I also changed sum so that rather than being limited to integers, it can be used for any type that conforms to AdditiveArithmetic (unsigned integers, 32-bit integers, floating point types, etc.). Likewise, for printValues, it now supports any sequence that has values that conform to CustomStringConvertible (i.e., anything that can be printed). In both cases, the idea is to make the extension as flexible as possible so it works with any type that is logical for the method in question.

    let total = [1.25, 2.3, 3.3].sum()
    ["moe", "larry", "curly"].printValues()
    

    A few more examples where I might favor a protocol over a particular type:

    • When writing a routine that takes string input, I might write it to accept StringProtocol, so that the routine accepts both strings and substrings, (e.g. string[start..<end]).

    • I employ a similar thought process when dealing with ranges, where the algorithm ideally should work on a closed range, open range, partial range, etc. I’ll use RangeExpression where I need some flexibility, and I’ll just use Range when I’m looking for something quick and simple.

    That having been said, often writing the method for a particular type is simpler than writing one for the protocol, so one should balance the flexibility afforded by using a protocol over the ease of writing the method for a particular type. Personally, if it’s some private method that won’t be used elsewhere, I’ll go ahead and write the easy, type-specific rendition, but if it’s some public/internal utility method that might enjoy some re-use, I’ll use the protocol renditions for greatest flexibility.

    Bottom line, when writing a method, ask yourself whether it’s only important that the method only works for Array types, or whether there are other sequences that the same routine might be useful.


    In terms of when I use Sequence or iterators for my own types, that’s a far less common. I’ll do this where I am writing a true “sequence”, where the next value is generated from the preceding value(s), e.g. a Fibonacci sequence or this count-and-say sequence. You can end up with some very natural interactions with the sequence at the calling point rather than shoe-horning the results into an array. But we don’t tend to encounter this pattern nearly as frequently as simple arrays in real-world applications.