Note2 min read

Three comparisons the standard library already does

Finding the first match, ordering by more than one key, and checking an index against a range. The hand-written version of each is longer, and two of the three are also slower.

Three things I keep rewriting by hand and keep finding already in the standard library. They are unrelated, except that in each case the longer form is the one I reach for first.

Finding the first match

let firstSeven = numbers.filter { $0 == 7 }.first

filter visits every element and allocates an array to hold the matches, then first takes one and the rest is discarded. On an array of a hundred where the match is at index two, that is ninety-eight comparisons that are not needed.

let firstSeven = numbers.first { $0 == 7 }

first(where:) stops at the first match and allocates nothing. Both are O(n) in the worst case, so this is not a complexity argument — the difference is that the cost of one tracks where the match is, and the cost of the other tracks the length of the sequence.

Ordering by more than one key

Sort contacts by surname, then by first name within each family. Written out, it is a chain of comparisons hinging on an equality check that is easy to get subtly wrong:

if lhs.surname != rhs.surname {
  return lhs.surname < rhs.surname
}
return lhs.name < rhs.name

Tuples of Comparable elements are themselves Comparable, and the comparison is lexicographic — exactly the rule you were writing:

extension Person: Comparable {
  static func < (lhs: Person, rhs: Person) -> Bool {
    (lhs.surname, lhs.name) < (rhs.surname, rhs.name)
  }
}

Adding a third key is adding a third element to each tuple. The nested version grows by four lines and a new chance to compare the wrong pair.

Checking an index against a range

if index >= 0 && index < items.count {

Two comparisons and an assumption — that indices start at zero, which is true for Array and not for ArraySlice, whose indices are inherited from the array it was sliced from. ~= is the pattern-match operator, the one switch uses for case 0..<10, and it is callable directly:

if 0..<items.count ~= index {

That is shorter, but it is the same zero-based assumption in fewer characters — it does not fix the slice. items.indices.contains(index) does, by asking the collection for its own bounds instead of constructing them:

if items.indices.contains(index) {

All three are cases where the standard library already provides the operation and the longer form is habit rather than necessity.

Topics