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.

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

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:

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:

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:

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:

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.

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!

  • Xavi Matos

    there’s another important practical reason to conform to Equatable.

    through constrained extensions in the standard library, you get a lot of free functionality when you conform to Equatable.

    compare your options for testing that a collection contains a particular value between a struct that conforms to Equatable and one that doesn’t.

    when conforming to Equatable, you can just pass a value as a parameter.

    when not, you have to pass a predicate block.

    i think the former is obviously preferable, and there are many more methods you get when you conform.

    checkout my playground to see what i mean in detail.
    https://github.com/CalQL8ed-K-OS/value-types-should-conform-to-Equatable

  • Kevin DeLeon

    Great post. I just never think about making some of my “value types” equatable in simple apps…but it makes sense to build that functionality in from the start.

  • Pingback: Swift: Equatable with Optionals()

  • Kametrixom

    If any value type should conform to the Equatable protocol, would it be possible for Apple to make all structs automatically conform to this protocol by using == for all value properties and === for all object properties? Or maybe just compare byte for byte in memory? Maybe there are some cases where this would be unpreferable but I can’t think of one

    • Andrew Bancroft

      For a good majority of the cases, I think the implementation you’ve described would be just fine. In that last section of the article where I address dealing with reference types, I mention that there may be times where we want to compare reference equality (ie, use ===), but there are other instances where that may _not_ be what we want to do, right?

      I’m thinking “equality” is one of those things that’s better not to make any assumptions about. You and I know the most accurate implementation of == that’s appropriate for our value Types…

      If Apple gave us a default implementation, I can imagine being extremely confused and frustrated by equality comparisons that produced unexpected results, just because my Struct held on to a few reference Types that I’d have different equality expectations about.

      Overall not a bad thought though, especially since Apple says that “Eeeevery single value Type out there should conform to Equatable”. I’m betting they just don’t want to risk negative fallout for over-generalizing what it would mean for one instance of a Type to be equal to another instance of that Type.