Exhaustive Properties with Tuples

One thing I really love about Swift is its exhaustive switch statement. As programmers, we live with a creeping dread of change and the unforeseen consequences it can wreck on our carefully calibrated computations. And switch is an excellent hammer with which we can nail down at least one future loose end.

If we have code that calculates how many spaces a character can move in a game, for example:

enum Character {
  case elf, dwarf, human
}


func spaces(for character: Character) -> Int {
  switch character {
  case .elf:
    return 7 // Spritely!
  case .dwarf:
    return 3 // Short legs; big heart.
  case .human:
    return 5 // Even Steven.
  }
}

…and we later add a dragon:

enum Character {
  case elf, dwarf, human, dragon
}

No problem! The Swift compiler let’s us know there’s a spot where we haven’t considered what to do with dragons, and won’t let us continue until we deal with it:

func spaces(for character: Character) -> Int {
  switch character { 🛑 Switch must be exhaustive
    //...
  }
}

This future-proofing is such a comfort to me, I cram everything I can in switches. But sometimes it’s just not feasible. Let’s look at a simple person model. It can hold a firstName and/or a lastName, and put those together in a description:

class Person: CustomStringConvertible {
  var firstName: String?
  var lastName: String?
  

  var description: String {
    return [firstName, lastName]
      .compactMap { $0 }
      .joined(separator: " ")
  }
}

It also has utility methods for letting us know if it has a full name, and for returning either the first or last name, whichever is present, for more informal modes of address:

extension Person {
  var isFullName: Bool {
    guard
      firstName != nil,
      lastName != nil else {
        return false
    }
    return true
  }
  

  var firstOrLast: String? {
    return firstName ?? lastName
  }
}

This all looks fairly straightforward, but I’m terrified. What if later we add a middleName property? Nothing will break, yet everything will be wrong. It will be up to whomever implements middleName to search for all the places it might be relevant1 and incorporate it.

How can we get switch-like exhaustiveness when dealing with the properties of a type? The first step (and our first clue) is to treat the properties as a set. What if we put them in a tuple?

class Person: CustomStringConvertible {
  var name: (first: String?, last: String?)
}

Accessing the properties of Person is still painless:2

let myPerson = Person()
myPerson.name.first = "Deedlit" 

And we can now rewrite description and isFullName in terms of the tuple:3

var description: String {
  let (first, last) = name
  return [first, last]
    .compactMap { $0 }
    .joined(separator: " ")
}


var isFullName: Bool {
  guard case (_?, _?) = name else {
    return false
  }
  return true
}

But firstOrLast, which only depends on a first and last name and doesn’t care if we ever add more, can reference the values of the tuple directly:

var firstOrLast: String? {
  return name.first ?? name.last
}

Now what happens if we add a middle name?

class Person: CustomStringConvertible {
  var name: (
    first: String?, 
    middle: String?, 
    last: String?
  )


  var description: String {
    let (first, last) = name
    🛑 tuples have a different number of elements
	// ...
  }
}



extension Person {
  var isFullName: Bool {
    guard case (_?, _?) = name else {
    🛑 tuples have a different number of elements
    }
	// ...
  }


  var firstOrLast: String? {
    // ...
  }
}

description and isFullName are both flagged with compiler warnings, firstOrLast just keeps on trucking, and I cut my antacid budget by 4×.

So, to be clear, habitually shoving all properties into a tuple won’t scale well. But it is a useful tactic to employ when dealing with models, mocks or anything that has:

  1. naturally constrained state,
  2. logic that depends on the shape of said state, and
  3. a strong affinity towards change in the face of shifting requirements

When we are confronted with such beasts, we can save our future-selves some consternation by popping properties into tuples.


1: Think of all the places extension Person could live if you really want to break out in a cold sweat.↩︎

2: Though, outside the forced reality of a blog example, the Person model should be a struct, its name should be a let, and everything immutable. Still, it’s good to know mutable tuples are a thing. ↩︎

3: The (_?, _?) in isFullName might look odd. It is a combination of the Wildcard Pattern and the Optional Pattern. It means, roughly, “a tuple of two non-optional things.” We’ve talked about the Optional Pattern before, over here.↩︎