Swift Equatable and Comparable

Not sure when you should make your Swift types Equatable? What about Comparable? In this post I look at two common situations where using the Swift Standard Library gets easier when you do.

Update 2 April 2018: Starting with Swift 4.1 you can have the compiler automatically synthesize Equatable conformance for your type. See How To Get Equatable And Hashable For Free.

A Simple Example

Let’s use a simple structure for a country that expects a name and capital and has a flag indicating if we have visited:

struct Country {
  let name: String
  let capital: String
  var visited: Bool
}

We can use the default initializer to create some examples:

let canada = Country(name: "Canada", capital: "Ottawa", visited: true)
let australia = Country(name: "Australia", capital: "Canberra", visited: false)

We can also already store these in an array (but not yet a Set or Dictionary - that needs hashing which is for another time):

let bucketList = [brazil,australia,canada,egypt,uk,france]

All Things Being Equal

What if we want to check if a specific country is in our bucket list array? Well the long way is to use the contains(where:) instance method of Array. This expects a predicate that returns a boolean indicating if an element of the array matches. So to test if our bucket list contains a specific country we could write:

let object = canada
let containsObject = bucketList.contains { (country) -> Bool in
  return country.name == object.name &&
  country.capital == object.capital &&
  country.visited == object.visited
}

This is a pain and as you probably already know there is an easier way. The Swift standard library has an Equatable protocol that we can adopt by adding the static == operator function to our type in an extension:

extension Country: Equatable {
  static func == (lhs: Country, rhs: Country) -> Bool {
    return lhs.name == rhs.name &&
    lhs.capital == rhs.capital &&
    lhs.visited == rhs.visited
  }
}

The == operator returns true when the two Country arguments are equal. Two Country values are equal if all visible properties of the values are equal.

let visited = Country(name: "Australia", capital: "Canberra", visited: true)
let unvisited = Country(name: "Australia", capital: "Canberra", visited: false)

visited == australia       // false
unvisited == australia     // true

When your type is Equatable it can make use of the simpler contains method of Array that takes the value we are testing for. No predicate or closure needed:

bucketList.contains(canada)  // true

Applying Order to the World

What if we want to sort our bucket list? To sort something we need to be able to compare two values. As with the Equatable example we can sort using a predicate closure (sorting just on name for brevity):

bucketList.sorted(by: { $0.name < $1.name } ) 

That works but using the standard library to sort an array is easier if our type is also Comparable. For that we need to add the static < operator function as well as the == operator we already have for Equatable. We can add both in a single extension (replacing the extension for Equatable):

extension Country: Comparable {
  static func == (lhs: Country, rhs: Country) -> Bool {
    return lhs.name == rhs.name &&
    lhs.capital == rhs.capital &&
    lhs.visited == rhs.visited
  }

  static func < (lhs: Country, rhs: Country) -> Bool {
    return lhs.name < rhs.name ||
    (lhs.name == rhs.name && lhs.capital < rhs.capital) ||
    (lhs.name == rhs.name && lhs.capital == rhs.capital && rhs.visited)
  }
}

What should the natural order for our Country type be? For the purposes of this post I sort alphabetically based on the name first and then the capital and finally unvisited before visited.

Now that our Country type is comparable we can again drop the closure with a predicate for the simpler form of sorted

bucketList.sorted()

All Done

That’s all there is to it. I used a value type (Swift struct) but the code is similar if we are working with a class. For me at least I find it easier to grasp the concept behind somewhat abstract protocols when I think in terms of what supporting the protocol allows me to do. In this case I added Equatable to simplify testing if my object was part of a collection and Comparable to more easily sort that collection.

Further Reading

From the Swift Standard Library