Strongly typed identifiers in Swift

In a lot of our code, we have structs that contain some sort of identifier. This is usually a serverside generated id, that uniquely identifies some record in a database.


struct Person {
  let id: String
  var name: String
  var age: Int?
}

struct Building {
  let id: String
  var title: String
  var owner: Person
}

Strings as identifiers

Sometimes these identifiers are UUIDs or Ints, but mostly they are Strings. Opaque to the client, but meaningful on the server.

Most functions in our codebase work on structs directly. But some use the identifiers, since they exist anyway:


func scrollToPerson(_ person: Person) {
}

func scrollToPerson(withId id: String) {
}

The scrollToPerson(_:) function is strongly typed, but the scrollToPerson(withId:) function isn’t.
This leads to accidentally mistakes, where we use the wrong identifier:


scrollToPerson(withId: mainBuilding.id) // Wrong
scrollToPerson(withId: mainBuilding.owner.id) // Correct

Strongly typed identifiers

We’ve recently begon creating strongly typed structs for these identifiers:


struct Person {
  struct Identifier: RawRepresentable, Hashable, Equatable {
    let rawValue: String
    init(rawValue: String) { self.rawValue = rawValue }
  }

  let id: Identifier
  var name: String
  var age: Int?
}

struct Building {
  struct Identifier: RawRepresentable, Hashable, Equatable {
    let rawValue: String
    init(rawValue: String) { self.rawValue = rawValue }
  }

  let id: Identifier
  var title: String
  var owner: Person
}

With this strongly typed Identifier in place, we no longer can accidentally use the wrong identifier.


func scrollToPerson(withId id: Person.Identifier) {
}

// This causes a type error:
scrollToPerson(withId: mainBuilding.id) 
                                    ^
// Cannot convert value of type 'Building.Identifier' to expected argument type 'Person.Identifier'

We implement the Hashable and Equatable protocols so we can use these identifiers in Sets and as Dictionary keys.
We use RawRepresentable so we get the Equatable implementation for free, Hashable is implemented in a protocol extension:


extension RawRepresentable where RawValue : Hashable {
  public var hashValue: Int { return rawValue.hashValue }
}

Wishlist: newtype

Haskell has a language feature that implements this pattern of wrapping an existing type to create a new type. It is called: newtype.
That would also be nice to have in Swift. It looks similar to typealias, but creates a new type, instead of just an alias. Then we could write:


struct Person {
  newtype Identifier = String

  let id: Identifier
  var name: String
  var age: Int?
}

Alternative: phantom types

As an alternative to creating multiple separate identifier types, we could create a single generic type, using a phantom type:


struct Identifier<T>: RawRepresentable, Hashable, Equatable {
  let rawValue: String
  init(rawValue: String) { self.rawValue = rawValue }
}

The generic type argument T here isn’t used in the Identifier type itself, it is only used to distinguish different identifiers.
When using this generic identifier, the previous example becomes:


struct Person {
  let id: Identifier<Person>
  var name: String
  var age: Int?
}

func scrollToPerson(withId id: Identifier<Person>) {
}

This does have the benefit of being shorter, and thus easier to write. But is it better?
I don’t think this code is easier to read than the previous version with Person.Identifier. In fact, I think it’s harder to read. This is a classic case of optimising for writing code, instead of optimising for reading the code.

To keep code maintainable in the long run, I thing we should strive for readability over writability. So I’ll stick with writing multiple separate types, and waiting for a newtype construct.

Alternative 2: A typealias

(Addition 2017-07-13)

A colleague suggested a second alternative; Using a typealias to keep te readability of scrollToPerson(withId id: Person.Identifier), but also using the generic single definition.


struct GenericIdentifier<T>: RawRepresentable, Hashable, Equatable {
  let rawValue: String
  init(rawValue: String) { self.rawValue = rawValue }
}

struct Person {
  typealias Identifier = GenericIdentifier<Person>

  let id: Identifier
  var name: String
  var age: Int?
}

func scrollToPerson(withId id: Person.Identifier) {
}

Maybe this is a nice middle ground. Although I would throw this GenericIdentifier<T> away, once Swift gains a newtype construct.

Rewrite code to make it stronger

From The Incomparable podcast episode 354: Sons of Caledonia.
Dan Moren talking about writing a novel (audio):

The amount of time you spent writing that first novel… rewriting it is going to be more, probably.
[…]

It’s like playing Jenga, right? It’s like: “Oh my God, if I take this brick out right here, will this whole thing fall over?”
[…]

It’s trying to get yourself out of this mindset of: “Oh, my story is this delicate spiderweb, and if I break this one strand, the whole thing will fall apart.”
And trying to get yourself more in the mindset of: “My story is like a piece of iron, that is being worked in a forge.”

Where you’re hammering on it to make it tempered, to make it stronger than the thing it was originally.
Like you take a sword, and it’s all about folding the metal, hammering it, and trying to reinforce it. To the point where it’s not going break immediately. You can hammer on something pretty hard. To try to sort of work in the right shape.

That’s what you’re going for, when you’re rewriting. Otherwise, you just sort of get paralysed with fear that anything that you change will break everything. That’s not the case, stories are pretty resilient in that way.
And you can always fix it, is the good news.

This really resonated with me when I heard it. I think this applies equally to writing code as it does to writing in general.

Writing code is like writing a novel. The first iteration of the code that passes the unit tests, is just the very first draft. Now you can start rewriting.