[swift unboxed]

Safely unboxing the Swift language & standard library


Equatable

Adventures in Swift value equality.

31 July 2018 ∙ Protocols ∙ written by

Equality and the Equatable protocol in Swift are all about value equality.

In our world of value types, if a = 5 and b = 5 then a and b are the same. Any place you pass in a, you could just as well pass in b and not notice any difference.

In other words, a == b.

Reference types can also have equal values but add the additional question of identity and the identity-of operator === (triple equals) into the mix. We’ll only consider value equality for this article.

What are the requirements of the Equatable protocol?

Equatable Protocol

If your type conforms to Equatable, you can compare two values of the type with == and !=.

The protocol has a single requirement: the == function:

public protocol Equatable {
  static func == (lhs: Self, rhs: Self) -> Bool
}

It takes two values, and returns a Bool saying whether they’re equal.

Thanks to a protocol extension, you don’t need to provide != as there’s a default implementation:

public static func != (lhs: Self, rhs: Self) -> Bool {
  return !(lhs == rhs)
}

If we wanted to screw it up, we could return a random Bool value each time for inconsistent results. We probably have some intuition that this would be bad, but what are the official invariants we should keep in mind?

According to the documentation, equality comparison results should have these three properties:

  • Reflexive: a value always equals itself; a == a must always be true.
  • Symmetric: the order of the values doesn’t matter; a == b means b == a.
  • Transitive: value relationships chain together; if a == b and b == c then a == c.

That covers the protocol requirements. How do we implement it for our own types?

Implementing Equatable

Back in the old days, you had to write oftentimes tedious code for ==:

func == (lhs: Dog, rhs: Dog) -> Bool {
  return lhs.name == rhs.name &&
         lhs.breed = rhs.breed &&
         lhs.age == rhs.age &&
         lhs.size == rhs.size &&
         // ... etc. 😓
}

Even worse were enumerations with associated values: you had to have a switch, check for each case, bind the values, and then do the == dance for each one.

Auto-Equatable

As of Swift 4.1, the compiler can generate the == function for you in some cases.

Given this example struct:

struct Puppy: Equatable {
  let name: String
  let age: Int
}

The compiler will be able to write == since both properties, String and Int themselves conform to Equatable.

Conditions for Conformance

What are the requirements to not have to write == yourself?

For classes, you’re out of luck; there’s no synthesized conformance and you always have to do it yourself. 😓

For structs, all stored properties must conform to Equatable. If the struct has no stored properties, then its instances will all equal each other (i.e. == will always return true).

For enumerations, it depends on the associated values.

  • If there are no associated values and just bare cases, then you can get Equatable conformance.
  • If there are associated values, then all of their types must conform to Equatable.

There’s one extra edge case: as of Swift 4.1, an enumeration with no cases will not get synthesized conformance (although this will change in a future version of Swift).

Of course you can always add your own == implementation, and then the compiler won’t bother to auto-generate one for you.

The Closing Brace

Equatable is a simple protocol and is even easier to work with thanks to automatic conformance in Swift 4.1.

If you’re curious how the compiler does the work to synthesize an implementation, you’re in luck! Check out the article Synthesized Conformance to Equatable for the details.

If you want more protocols, four other protocols build on top of Equatable:

Learn about one protocol, and more spring up. Turns out it’s protocols all the way down. 🐢

}