I am the author of iOS 17 Fundamentals, Building iOS User Interfaces with SwiftUI, and eight other courses on Pluralsight.
Deepen your understanding by watching!
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:
- The comparison must be reflexive
- The comparison must be symmetric
- 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 name
s, latidude
s, and longitude
s 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
Place
s equal if both of them point to the samefeatureImage
(ie, should we just use===
to check and see if the references are the same)? - OR, are the two
Place
s equal if both of theirfeatureImage
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!