Every Swift Value Type Should Be Equatable

As I listened to the WWDC15 talk on Building Better Apps with Value Types in Swift I was struck by a sentence that I had never dawned on me before:

Every Value Type should be Equatable.

That is, every Value Type should conform to the Equatable protocol.

Talk about a sweeping statement! Wow – every Value Type should be Equatable? Hmm… Let’s unpack the “why’s” and “how’s” of this statement.

Why?

I’d never thought about why I might want my Value Types in Swift to be Equatable. Not that I thought it’d be a terrible idea to implement the == operator for a Type… I just never realized that this was actually expected behavior of Value Types!

The reasoning in the talk was that Values are intuitively meant to be compared for equality. Because they’re Values, there is inherent expectation from clients of the Type to be able to ask and know if one Value is equal to another Value of the same Type.

We naturally expect to be able to ask two variables/constants, each holding Int Values (because in Swift, Int is a Value Type), if they equal each other. And we naturally expect the comparison to compare the actual numbers… the Values themlselves.

1let a = 10
2let b = 5 + 2 + 3
3a == b // true
4
5let x = 1
6let y = 2
7x == y // false

Likewise, we naturally expect to ask two Strings if they’re equal:

1let str1 = "I love Swift!"
2let str2 = "I love Swift!"
3str1 == str2 // true
4
5
6let str3 = "i love swift!"
7str1 == str3 // false - case-sensitive comparison

In fact, we naturally expect to ask these kinds of equality questions about any of the Swift standard library Value Types, don’t we?

How?

We do expect to test for equality between two Value Types. It just makes sense.

So now the question is, “How?”

The simple answer is that our Value Types need to implement an == operator. But there’s something really important to consider:

Properties of equality

To be truly equal, the == operator not only needs to be implemented, but it needs to be implemented in such a way that it behaves as we’d expect when doing our comparisons. During the talk, Doug mentioned three important properties of equality that need to hold for our Value Types:

  1. The comparison must be reflexive
  2. The comparison must be symmetric
  3. The comparison must be transitive

That sounds awfully “math-y”. In fact, it’s the exact same terminology used in mathematics. But don’t worry, the terminology is simple and natural to understand.

Reflexive

To be reflexive, the Type’s == operator needs to make sure that the expression x == x returns true.

So if I have let x = 1 and I write the expression x == x, I do in fact get true because Int‘s equality operator is reflexive (as expected).

Symmetric

To be symmetric, the Type’s == operator needs to compute things in such a way that the expression x == y and y == x return the same value.

Here’s an example of symmetry:

 1let x = 1
 2let y = 1
 3
 4x == y // true
 5y == x // true
 6
 7let str1 = "Hi"
 8let str2 = "Hello"
 9
10x == y // false
11y == x // false

Transitive

Finally, to be transitive, the Type’s == operator needs to compute things in such a way that when x == y is true, and y == z is true, then x == z is also true.

Here’s an example of transitivity:

1let x = 100
2let y = 50 + 50
3let z = 50 * 2
4
5x == y // true
6y == z // true
7x == z // true

Implementation

Most of the time, the implementation of == is very simple. If your Value Type is comprised of other Value Types that have an == operator that’s correctly implemented with the semantics I just described, then the implementation for your Type is straight-forward.

An example might help to set things up for understanding. Suppose that we’re building a sight-seeing app for a local tourism company. We’ve got a struct called Place to help us encapsulate the idea of… well… a “place” to visit. It looks something like this:

1struct Place {
2    let name: String
3    let latitude: Double
4    let longitude: Double
5
6    // init is auto-generated by the compiler in this case
7}

Since Place is a Value Type (Struct) which is comprised of other Value Types, you’d simply need to do something like the following to make it Equatable:

1extension Place: Equatable {}
2
3func ==(lhs: Place, rhs: Place) -> Bool {
4    let areEqual = lhs.name == rhs.name &&
5        lhs.latitude == rhs.latitude &&
6        lhs.longitude == rhs.longitude
7    
8    return areEqual
9}

One of the first things to notice is that the == operator has to be implemented as a stand-alone global function, rather than as part of the Type definition.

Notice also that even though we have the source for the Type that we want to make Equatable, I chose to signal the Equatable protocol adoption through an extension on the Type, rather than at the Type declaration itself. Both are acceptable, but it’s become convention to use the extension strategy for this particular protocol.

The implementation of == uses the intuitive semantics that one Place isn’t the same as another Place unless the names, latidudes, and longitudes are all the same.

lhs and rhs simply mean “left-hand side” and “right-hand side”, respectively. Since there’s a Place instance on the left-hand side of the == operator, and a Place instance on the right-hand side of the == operator when we use it in practice, it makes sense to label these parameters according to that pattern.

The implementation could literally be read as, “If the Place on the left-hand side’s name is equal to the Place on the right-hand side’s name, AND … the latitude … AND … the longitude, then the two Place instances are equal.”

Dealing with reference types

If Reference Types are involved with your Value Type implementation, things could get a little more complicated. “Complicated” probably isn’t the right word… but you do have to think a little more about your Type’s equality semantics.

Let’s modify the example just a little bit:

Supposing that Place had an additional property called featureImage which held a reference to a UIImage instance (a Reference Type), we’d need to test for equality a little bit differently. And how we test for equality depends on the particulars of our Type’s equality semantics:

  • Are the two Places equal if both of them point to the same featureImage (ie, should we just use === to check and see if the references are the same)?
  • OR, are the two Places equal if both of their featureImage instances contain the same underlying bitmap (ie, they’re the same picture in essence)?

As you can see, the phrase “it depends” applies here. Certainly we need to test for some kind of equality on the featureImage in order to have a complete == implementation. But how we go about it really comes down to the semantics that you and others would expect from asking the question, “Is this Place equivalent to that Place?”

For this example, I’m going to go with the latter statement: that two Places are equal if both of their featureImage instances contain the same underlying bitmap.

 1extension Place: Equatable {}
 2
 3func ==(lhs: Place, rhs: Place) -> Bool {
 4    let areEqual = lhs.name == rhs.name && 
 5            lhs.latitude == rhs.latitude &&
 6            lhs.longitude == rhs.longitude &&
 7            lhs.featureImage.isEqual(rhs.featureImage) // depends on your Type's equality semantics
 8
 9    return areEqual
10}

Wrapping up

Every Value Type should conform to the Equatable protocol. In this article, we unpacked the “why’s” and the “how’s” of this fundamental characteristic of Value Types. From here, we’ve all got to jump on board and ensure that we meet this expectation in our code!

comments powered by Disqus