Lazy Cartesian Products in Swift

Would you like to play a game?

Let’s play some battleship! Assuming a standard 10x10 board, we’ll need two collections:

let xs = 1...10
let ys = ["A", "B", "C", "D", "E", 
          "F", "G", "H", "I", "J"]

If we wanted to hit every square on the grid, we’d have to iterate over both lists in a nested loop. 1

for x in xs {
  for y in ys {
    target(x, y)
  }
}

There’s nothing wrong with this as far as it goes. But it doesn’t do a great job of expressing what we’re actually trying to accomplish — that is, iterating over every ordered pair of ys and xs.

A more declarative way to go about our task might be to generate a new collection which is explicitly the cartesian product of the two sets, and iterate over that:

product(xs, ys).forEach { x, y in
  target(x, y)
}

Great! Only one minor problem: product doesn’t exist.

Force Majeure

Building something like product isn’t super hard. The trickiest part is getting all the generics right so it can be used with, say, a closed range just as well as with an array.

func product<X, Y>(_ xs: X, _ ys: Y) ->
  [(X.Element, Y.Element)]
  where X: Collection, Y: Collection {
    var orderedPairs: [(X.Element, Y.Element)] = []
    for x in xs {
      for y in ys {
        orderedPairs.append((x, y))
      }
    }
    return orderedPairs
 }

But when we calculate all the ordered pairs of two sets, the size of the resultant set is (as the name implies) the product of both sets. That’s no big deal when we’re talking a 10x10 grid. But if we’re dealing with 10k elements, we can benefit from a more lazy approach that stores the individual sets and generates their products on the fly, as needed.

Wall’s First Virtue

Let’s start by building an iterator:

public struct CartesianProductIterator<X, Y>: 
     IteratorProtocol where
     X: IteratorProtocol, 
     Y: Collection {

  public typealias Element = (X.Element, Y.Element)
 
  public mutating func next() -> Element? {
    //...
  }
}

Why is our generic type X an iterator but we force Y to conform to Collection? If you look at the nested loop in our naive example above, you’ll see that while we iterate over xs only once, we actually loop over ys a number of times (an xs.count number of times, to be precise).

IteratorProtocol allows us to iterate over a set exactly once, so it’s perfect for our X type. But only Collection guarantees us the ability to non-destructively traverse a sequence over and over. So Y must be a little more constrained.

Let’s add an initializer to store our iterator, collection, and related curiosities as properties:

private var xIt: X
private let yCol: Y
 
private var x: X.Element?
private var yIt: Y.Iterator

public init(xs: X, ys: Y) {
  xIt = xs
  yCol = ys
 
  x = xIt.next()
  yIt = yCol.makeIterator()
}

First note xIt is a var. Iterators mutate themselves in the course of serving up next(), so our copy of xs must be mutable.

Also, our ultimate goal here is to take values from xIt and for each of them iterate over the all the values of yCol. We prep for this by pulling the first value out of xIt into x and making an iterator for yCol called yIt.

And note x needs to be optional. We’ll ultimately iterate over xIt until we hit the end — and we’ll know we hit the end when x is nil.2

With all that settled, let’s move on to our implementation of next().

The NeXT Step

The first step of next() is simple; pull a value out of yIt each time it’s called and pair it with the same ol' x we set in the initializer (providing, of course, x isn’t nil)

public mutating func next() -> Element? {
  guard let someX = x else {
    return nil
  }
 
  guard let someY = yIt.next() else {
    return nil
  }
 	 
  return (someX, someY)
}

There. Now each call to next() returns x and whatever the next value of yIt is. But what do we do once we hit the end of yIt? We want to bump x to the next value of xIt, create a new yIt from our collection — and then do the whole thing over again.

Anytime we say to ourselves “…and then do the whole thing over again,” it’s a sign recursion is in our future.

The End is the Beginning is the End

There’s nothing magical about recursion. To do it, we just need to call a method from within the implementation of itself. We’ll do it here when we run out of values in yIt:

public mutating func next() -> Element? {
  guard let someX = x else {
    return nil
  }
 
  guard let someY = yIt.next() else {
    return next() //Recursion!
  }
 	 
  return (someX, someY)
}

But there are two things we need to pay attention to whenever we write recursive routines.

The first is making sure we don’t loop indefinitely. We’ll do that by setting conditions for terminating the recursion, and then making sure we move towards that condition with each iteration.

Our termination condition already exists. It’s that top guard. If x is ever nil, we’re out.

So all we have to do is make sure we’re moving towards a state where x is nil:

public mutating func next() -> Element? {
  guard let someX = x else {
    return nil
  }
 
  guard let someY = yIt.next() else {
	    yIt = yCol.makeIterator()
	    x = xIt.next()
	    return next()
	  }
 	 
  return (someX, someY)
}

There. Now every time we hit the end of yIt, we not only make a new one from our collection, but we also pull the next x from xIt. Eventually, xIt will run out of elements, and x will be nil. End of recursion.

Adventures in Meta

The second thing to look out for when writing recursively is blowing the stack. A stack overflow happens when you call too many functions in a row without returning.3

We can easily see how recursion might cause this to happen. Basically, when we call next() from inside next() we dig one level deeper in the stack. If calling next() in next() causes us to call next(), now we’re another level deep. Get too deep, and we error out with a busted stack.

Thankfully we can see the only time we call next() from inside next() is when yIt.next() returns nil. So the only way we can dig deeper in the stack is if yIt.next() returns nil many times in a row. And the only way that could happen is if yCol is empty.4

So we’ll short circuit that specific case with a guard:

public mutating func next() -> Element? {
  guard !yCol.isEmpty else {
    return nil
  }
 
  guard let someX = x else {
    return nil
  }
 
  guard let someY = yIt.next() else {
	    yIt = yCol.makeIterator()
	    x = xIt.next()
	    return next()
	  }
 	 
  return (someX, someY)
}

Free as in Function

And so, finally, we can lazily iterate over every ordered pair of our collections — all while only storing the collections themselves, not their product. But the syntax is a bit awkward:

let xs = 1...10
let ys = ["A", "B", "C", "D", "E", 
          "F", "G", "H", "I", "J"]
var it = CartesianProductIterator(
     xs: xs.makeIterator(), 
     ys: ys)
while let (x, y) = it.next() {
  target(x, y)
}

Let’s wrap this in a Sequence to get access to for...in, forEach, and the rest.

public struct CartesianProductSequence<X, Y>: 
     Sequence where 
     X: Sequence,
     Y: Collection {
  public typealias Iterator =
       CartesianProductIterator<X.Iterator, Y>
  
  private let xs: X
  private let ys: Y
  
  public init(xs: X, ys: Y) {
    self.xs = xs
    self.ys = ys
  }
  
  public func makeIterator() -> Iterator {
    return Iterator(xs: xs.makeIterator(), 
                    ys: ys)
  }
}

And as a finishing touch let’s add a a top-level function to make chaining little more readable:

public func product<X, Y>(_ xs: X, _ ys: Y) ->
     CartesianProductSequence<X, Y> where 
     X: Sequence, 
     Y: Collection {
  return CartesianProductSequence(xs: xs, ys: ys)
}

And just like that, our battleship dreams have become reality:

let xs = 1...10
let ys = ["A", "B", "C", "D", "E",
          "F", "G", "H", "I", "J"]

product(xs, ys).forEach { x, y in
  target(x, y)
}

As ever, here’s a gist of all this together in one place. Compliments of the house.


1: I know Battleship™ was invented before Descartes or something, and insists on calling out the lettered Y axis before the numbered X one. I tried a version of this post where the imaginary target(_:_:) function took its y parameter before the x. I was constitutionally incapable of publishing it.↩︎

2: Or it could be nil right now. If xIt is an iterator over an empty set, x will be nil by the end of initialization.↩︎

3: Ironically, I didn’t find the answer here particularly compelling.↩︎

4: Specifically: if yCol is empty, then every time we call next(), we’re going to immediately call return next() over and over again until we hit the end of xIt. If xIt is long, this could easily be enough to overflow the stack.↩︎