1

I'm implementing a few custom enumerators for my data model, e.g.

struct Land {
  // …
  let council: Council
  let size: Int
}

extension Collection<MyModel> {
  func lands(in council: Council, above size: Int) -> [Land] {
    guard isEmpty == false else {
      return []
    }
    filter { $0.council == council && $0.size > size }
  }
}

Then I thought if the built-in enumerators also takes a shortcut by checking if the collection is empty, then I don't have to do it. And this isn't limited to filter, but all the enumerators in general. For example, the doc for find(where:) says it has complexity of O(n), which mean O(0) if the collection is empty, but what does that mean exactly, does it skip the loop or still start the loop but finish at the very beginning? Is there any benefit for guarding agains empty collection before calling the enumerator?

HangarRash
  • 7,314
  • 5
  • 5
  • 32
Heuristic
  • 5,087
  • 9
  • 54
  • 94
  • 1
    None of the collection methods will *crash* if the collection is empty. You don't need to guard against an empty collection. What are you trying to prevent here? – Sweeper Jul 25 '23 at 01:53
  • 1
    Not sure what is your doubt. If the collection is empty what's is your concern ? If it is a for loop it will simply skip it. Same applies to filter map, forEach, and so on. – Leo Dabus Jul 25 '23 at 01:54
  • Thanks @Sweeper, I'm mainly curious about how it's handled internally, does it skip the work by first checking if it's empty or does it perform the enumeration and let it finish regardless of whether the collection is empty. – Heuristic Jul 25 '23 at 02:00
  • "does it skip the loop or still start the loop but finish at the very beginning" Just to clarify, the difference between the two is a constant time operation (one which doesn't differ based on the size of the collection), so they're both ultimately constant time `O(1)` (which is equivalent to `O(0)`, btw.). – Alexander Jul 25 '23 at 02:01
  • 1
    @Heuristic For implementation details like this, you would need to read the [source code](https://github.com/apple/swift). Either of these ways is a *correct* implementation. Some collections could potentially check `isEmpty` first before they call `makeIterator` because `makeIterator` is costly. Though, that raises the question of why they don't check `isEmpty` in `makeIterator` first, and do the costly thing only if it is not empty. – Sweeper Jul 25 '23 at 02:05
  • @Heuristic If you are working with arrays only you should be fine. It conforms to `RandomAccessCollection` and thats what really matters. – Leo Dabus Jul 25 '23 at 02:22

1 Answers1

1

Any Collection implementation can implement these methods however they like, as long as they are correct.

You can go to the Swift GitHub repository to see how each of the built-in collections are implemented.

I will focus on the default implementations of the Collection and Sequence (which Collection inherits) methods in this answer. See CollectionAlgorithms.swift and SequenceAlgorithms.swift.

None of the default implementations that involve iteration checks isEmpty. In the Collection methods, iteration is generally implemented like this:

var i = self.startIndex
while i != self.endIndex {
  ...
  self.formIndex(after: &i)
}

If the collection is empty, startIndex will be equal to endIndex. Consequently, the loop does not run.

For the Sequence methods, the only way available to iterate through a Sequence is to makeIterator and repeatedly call next. It is always some form of:

var it = makeIterator()
while let e = it.next() {
  ...
}

Note that a for loop is basically syntactic sugar for something like the above.

next implicitly checks for emptiness. If there is no elements, it returns nil. Consequently, the loop does not run. Also note that Sequence does not require an isEmpty method, so these methods cannot check emptiness this way.

Whether you classify this as "skip the loop" or "still start the loop but finish at the very beginning" is up to you. Those descriptions are equivalent as far as I'm concerned.

Sweeper
  • 213,210
  • 22
  • 193
  • 313