How do you write your own Collection, and what do you get for free?

2.1.11 · L5 · Learning

Predict first

Does this compile?

struct Countdown: RandomAccessCollection {
    let from: Int
    var startIndex: Int { 0 }
    var endIndex: Int { from }
    subscript(i: Int) -> Int { from - i }
}
  • Yes
  • No — index(after:) is missing

Mental model. You hand the standard library a map of your data: where it starts, where it ends, and how to step. It builds every tool on top.

The minimum is four requirements: startIndex, endIndex, a read-only subscript(position), and index(after:). From those you get iteration, map, filter, first, isEmpty, indices, and slicing (the default subsequence is Slice<Self>).

The defaults are correct but not always fast: count walks the indices, O(n). If your index is an Int and you adopt RandomAccessCollection, Swift supplies O(1) offsetting, so you only write startIndex, endIndex, and the subscript. Remember endIndex is one past the last element, and a slice of your collection keeps your indices.

With Int indices a RandomAccessCollection inherits index(after:), count and the rest. Three members are enough.

Code

struct Ring<Element>: RandomAccessCollection {
    var storage: [Element]
    var head: Int
    var startIndex: Int { 0 }
    var endIndex: Int { storage.count }
    subscript(i: Int) -> Element { storage[(head + i) % storage.count] }
}

let r = Ring(storage: [1, 2, 3, 4], head: 2)
print(Array(r), r.count, r.map { $0 * 10 })
[3, 4, 1, 2] 4 [30, 40, 10, 20]

Common traps

  • Off-by-one endIndex. It is the position after the last element, not the last element.
  • Claiming RandomAccess with O(n) steps. The protocol promises O(1); algorithms rely on that.

They might follow up with

  • When would you override count? When you can answer it in O(1) but the default would walk the indices.
  • Why define your own index type? To stop callers mixing indices from different collections, or to carry extra state such as a tree position.

Related questions

  • 2.1.9 Sequence, Collection, BidirectionalCollection, RandomAccessCollection: what does each add?
  • 2.1.6 What is an ArraySlice, and why does slice[0] crash?

← What does .lazy do, and what does it cost?