• Skip to main content
  • Skip to footer

Khawer Khaliq

  • Home

Swift Protocols Don’t Play Nice With Equatable. Or Can They? (Part One)

Share
Tweet
Share
Pin

*** Updated for Swift 4.1 ***

This is Part One of a two-part article. This part demonstrates how making protocols conform to Equatable renders us unable to use the protocols as types and why trying to implement equality functions on protocols, even without Equatable conformance, can lead to erroneous results in some cases and is impossible in others.

Part Two uses type erasure to implement equality comparisons and Equatable conformance at the protocol level. This allows us to program to abstractions using protocol types while being able to safely make equality comparisons and also use functionality provided by the Swift Standard Library which is available only to types that conform to Equatable.

Contents

Fruit, anyone?
Programming to interfaces, not implementations
Protocols with Self requirement
Compromises, compromises
Does this really make sense!
Protocols are meant to be about behavior, aren’t they?
Conclusion

In a landmark WWDC 2015 talk titled “Protocol-oriented Programming in Swift” a.k.a. “the Crusty talk”, Dave Abrahams declared that Swift is a Protocol-oriented programming language. I would leave the details of how this differs from traditional object-oriented programming for another post. In essence, this entails using protocols to represent abstractions and favouring value types over reference types, unless the entity you are modeling specifically requires reference semantics.

Another piece of advice that came along with this new paradigm is to make all value types Equatable, or to be more exact, conform to the Equatable protocol. This makes sense as the concept of attribute-based equality is one of the defining characteristics of values, as we see for values we commonly use such as integers, decimals, strings, etc.

Fruit, anyone?

Let’s say we are working in a domain dealing with fruit. To keep things simple, let’s say that each piece of fruit has two characteristics, its weight and grade, both integer values. To keep things nice and abstract and to avoid having to deal directly with concrete types, we declare a protocol.

protocol Fruit {
    var weight: Int { get }
    var grade: Int { get }
}

Next we declare two concrete types representing apples and oranges respectively, both conforming to our protocol.

struct Apple: Fruit, Equatable {
    let weight: Int
    let grade: Int
}

struct Orange: Fruit, Equatable {
    let weight: Int
    let grade: Int
}

Note that we have also declared Equatable conformance for both our concrete types. This means that any two apples or oranges having equal weights and grades will be considered equal.

Programming to interfaces, not implementations

This age-old advice originated in the world of object-oriented programming and applies equally in the protocol-oriented realm. This is good programming practice in that it makes code more flexible, extensible and less brittle. The consequence of programming to interfaces is that most of our code would deal in abstract fruits, rather than concrete apples and oranges.

func doSomethingWithFruit(fruit: Fruit) {}
var storeFruit: [Fruit] = []

The actual creation of concrete instances would presumably be handled by factory methods which return the instances typed as Fruit so we can happily program to the abstraction rather than worrying about which concrete type of fruit we are dealing with.

At some point, though, we are going to have to compare two instances for equality. Now, both our concrete types implement Equatable, so had we been dealing with the concrete types, comparing for equality would have been a cinch. However, what we are dealing with are instances typed as Fruit.

No sweat. We will simply implement an equality function for our protocol and modify our protocol definition to add conformance with Equatable.

protocol Fruit: Equatable {
    var weight: Int { get }
    var grade: Int { get }
}

func ==(lhs: Fruit, rhs: Fruit) -> Bool {
    return lhs.weight == rhs.weight && lhs.grade == rhs.grade
}

It is noteworthy that, unlike concrete types, protocols do not automatically synthesize Equatable conformance by simply declaring it in the protocol definition. This should perhaps be the first hint that this is not something we should be doing.

Protocols with Self requirement

Sure enough, the moment we add Equatable conformance to our protocol, things start to go awry. All our functions, arrays, etc., that use the Fruit protocol as a type show the following error:

error: protocol 'Fruit' can only be used as a generic constraint because it has Self or associated type requirements

This error looks rather cryptic at first, but becomes easy to understand once we inspect the requirements of the Equatable protocol at bit more closely.

static func ==(Self, Self)

Self in the above method signature refers to the concrete type of the actual instances being compared and means that the concrete type of both the arguments must be the same. This is okay for structs or classes but cannot be guaranteed for protocols since there could be any number of concrete types which may adopt a given protocol and we cannot guarantee that any two instances being compared will always have the same concrete type. Swift deals with this issue by ensuring that any protocol that conforms to Equatable can no longer be used as a type.

Compromises, compromises

Now we are at an impasse. We either let go of Equatable conformance for our protocol or we will not be able to use our protocol as a type.

Equatable conformance is important to have because a host of functionality provided by the Swift Standard Library relies on Equatable conformance. For instance:

  1. If we want to check two arrays for equality, their elements need to conform to Equatable. The same is true if we want to check whether an array contains a given value, without having to provide a closure to determine equivalence.
  2. The Comparable protocol, used to sort elements of a collection, inherits from Equatable.
  3. The Hashable protocol, to which keys of a Dictionary and members of a Set must conform, inherits from Equatable.

Having said that, the main benefit of working with protocols is that they allow us to create behavior-rich abstractions that can make our code more decoupled and more easily extensible. Therefore, inability to use our protocol as a type is a show-stopper. So we bite the bullet and remove Equatable conformance from our protocol. This gets rid of the errors and we get to keep our equality function. We also add a function to ensure that we can compare any two instances types as Fruit for inequality.

func !=(lhs: Fruit, rhs: Fruit) -> Bool {
    return !(lhs == rhs)
}

Note that an inequality function gets synthesized automatically for types that conform to Equatable.

Does this really make sense!

Armed with our equality function, we do some testing.

let apple: Fruit = Apple(weight: 10, grade: 2)
let secondApple: Fruit = Apple(weight: 10, grade: 2)
let thirdApple: Fruit = Apple(weight: 15, grade: 2)
let orange: Fruit = Orange(weight: 10, grade: 2)

print(apple == secondApple) // true
print(apple == thirdApple)  // false
print(apple == orange)      // true

The first two results are expected but the third literally equates apples and oranges! This is because the equality method of the protocol is simply checking the values of the properties weight and grade regardless of the concrete types of the instances being compared. We could use generics to ensure that only instances of the same type can be compared for equality.

func ==<T: Fruit>(lhs: T, rhs: T) -> Bool {
    return lhs.weight == rhs.weight && lhs.grade == rhs.grade
}

But this does not help as it can only be used to compare for equality instances of types that conform to protocol Fruit, not instances typed as Fruit. Instead, we can solve the problem of comparing apples with oranges by modifying the equality function presented earlier to add a guard clause.

func ==(lhs: Fruit, rhs: Fruit) -> Bool {
    guard type(of: lhs) == type(of: rhs) else { return false }
    return lhs.weight == rhs.weight && lhs.grade == rhs.grade
}

Not very elegant, but at least now apples are no longer equal to oranges.

print(apple == orange)      // false

Let’s assume that we now have to cater not just to apples sourced locally but also to apples imported from other countries. So we add a new type to represent imported apples which has a property to identify the country of origin. Imported or not, apples are fruit so our new type also conforms to the Fruit protocol.

struct ImportedApple: Fruit, Equatable {
    let weight: Int
    let grade: Int
    let countryOfOrigin: String
}

Note that the automatically synthesized Equatable conformance for ImportedApple will take into account the country of origin in addition to the weight and grade. This will ensure, for instance, that apples imported from Australia do not end up being equal to those imported from South Africa.

However, when we have imported apples typed as Fruit, which is the likely scenario since we prefer to write code in terms of abstractions, we would have the following result.

let appleFromAustralia: Fruit = ImportedApple(weight: 10, grade: 2, countryOfOrigin: "Australia")
let appleFromSouthAfrica: Fruit = ImportedApple(weight: 10, grade: 2, countryOfOrigin: "South Africa")

print(appleFromAustralia == appleFromSouthAfrica)  // true

This illustrates just one one way in which adding a new concrete type which conforms to the same protocol but adds a type-specific property could lead to erroneous results being given by the equality function written in terms of the protocol type. And this is just a trivial example. In a domain of any complexity, it would be difficult to keep track of or even to anticipate all the errors that may be introduced by protocol-level equality comparisons.

Protocols are meant to be about behavior, aren’t they?

Yes, they are. Protocols are the tools for building rich behavioural abstractions, leaving implementation details to the concrete conforming types. Therefore, it is common to find protocols that only declare methods and no properties.

Here is an example of a protocol representing a shape that knows how to draw itself. The protocol does not say anything about what the shape should be or what data conforming types may store to be able to draw themselves, only that types that conform must implement a method to draw themselves.

protocol Shape {
    func draw()
}

Any type representing a shape that knows how to draw itself can conform to this protocol. To illustrate, we create two concrete types, one representing a rectangle and the other representing a circle.

struct Rectangle: Shape, Equatable {
    func draw() { ... }
    
    let topLeftCorner: CGPoint
    let length: Double
    let width: Double
}

struct Circle: Shape, Equatable {
    func draw() { ... }
    
    let centre: CGPoint
    let radius: Double
}

Now then, how do we apply a protocol-level equality comparison when the protocol does not declare any properties? The short answer is that we can’t. Each conforming type can be Equatable (in fact both our concrete types are) but we don’t have a way of checking equality at the level of the protocol since the protocol does not declare any properties.

Conclusion

Having worked through these examples and seen that trying to implement equality for protocols is fraught with possibilities of errors in some cases and just not possible in others, we can appreciate why protocols that are to be used as types cannot conform to Equatable. We could work around this limitation by sacrificing Equatable conformance in favour of a simple equality function, but it would likely produce correct results only under carefully controlled conditions and cannot be trusted to work correctly in real-world domains. Besides, this approach completely falls apart in cases where the protocol does not declare any properties.

This concludes Part One of this article. Do check out Part Two, which uses type erasure to implement equality comparisons and Equatable conformance at the protocol level. This allows us to program to abstractions using protocol types while being able to safely make equality comparisons and also use functionality provided by the Swift Standard Library which is available only to types that conform to Equatable.

Subscribe to get notifications of new posts

No spam. Unsubscribe any time.

Reader Interactions

Comments

  1. Ankur says

    February 16, 2022 at 6:18 am

    Thank you so much for putting it out so beautifully.

    Reply
    • Khawer Khaliq says

      February 17, 2022 at 6:06 am

      Thanks Ankur. Glad that you liked the article.

      Reply
  2. Alexey says

    October 6, 2022 at 7:44 am

    Java people live with Object.equals instance method for years. And that would work in Swift too. Having all this complexity for such a strict type checking in Swift just isn’t required in solving daily tasks. It gives more headaches instead.

    Reply

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Footer

Protocol-Oriented Programming (POP) in Swift

Use protocol-oriented programming to think about abstractions in a completely different way, leveraging retroactive modeling to introduce appropriate abstractions at any point in the development cycle, and creating traits that can let types opt into functionality simply by conforming to a protocol.

Pattern Matching With Optionals in Swift

The optional pattern explained in detail, including how to use it with a variety of conditional statements and loops, and how to add extra conditions when required, with a section on creating more complex pattern matching code involving optionals.

Test-Driven Development (TDD) in Swift

Learn how to use Test-Driven Development (TDD) in Swift which not only enables you to write more reliable and maintainable code but also allows refactoring of code in small increments and with greater ease and confidence.

Unwrapping Optionals With Optional Binding in Swift

Learn how to use optional binding to extract the value wrapped by an optional to a constant or variable, as part of a conditional statement or loop, exploring where optional chaining may be used in place of optional binding, and where these techniques can be used together.

Unit Testing and UI Testing in Swift

Learn how to use unit testing to gain confidence in the correctness of code at the unit level, and use UI testing to ensure that the application fulfills user requirements, explained in detail and illustrated using an example application built using SwiftUI.

When and How to Use the Equatable and Identifiable Protocols in Swift

Detailed coverage of the Equatable and Identifiable protocols, and how they can be used to model not only values but also domain entities with identity using Swift value types, to create code that is more efficient, easier to reason about, easily testable and more concurrency-friendly.

The Power of Optional Chaining in Swift

Learn how to use optional chaining to work safely with optionals, to set and retrieve the value of a property of the wrapped instance, set and retrieve a value from a subscript on the wrapped instance, and call a method on the wrapped instance, all without having to unwrap the optional.

What Are Swift Optionals and How They Are Used

This article explains what Swift optionals are, why Swift has them, how they are implemented, and how Swift optionals can be used to better model real-world domains and write safer and more expressive code.

A Protocol-Oriented Approach to Associated Types and Self Requirements in Swift

Use protocol-oriented programming to avoid having to use associated types in many situations but also to effectively use associated types and Self requirements, where appropriate, to leverage their benefits while avoiding the pitfalls.

Encapsulating Domain Data, Logic and Business Rules With Value Types in Swift

Leverage the power of Swift value types to manage domain complexity by creating rich domain-specific value types to encapsulate domain data, logic and business rules, keeping classes lean and focused on maintaining the identity of entities and managing state changes through their life cycles.

Rethinking Design Patterns in Swift – State Pattern

The State pattern, made simpler and more flexible with the power of Swift, with a detailed worked example to illustrate handling of new requirements, also looking at key design and implementation considerations and the benefits and practical applications of the pattern.

Conditional Logic With and Without Conditional Statements in Swift

Shows how to implement conditional logic using conditional statements as well as data structures, types and flow control mechanisms such as loops, balancing simplicity and clarity with flexibility and future-proofing.

Better Generic Types in Swift With the Numeric Protocol

Covers use of the Numeric protocol as a constraint on type parameters of generic types to ensure that certain type parameters can only be used with numeric types, using protocol composition to add relevant functionality as required.

Understanding, Preventing and Handling Errors in Swift

Examines the likely sources of errors in an application, some ways to prevent errors from occurring and how to implement error handling, using the error handling model built into Swift, covering the powerful tools, associated techniques and how to apply them in practice to build robust and resilient applications.

When and How to Use Value and Reference Types in Swift

Explores the semantic differences between value and reference types, some of the defining characteristics of values and key benefits of using value types in Swift, leading into a discussion on how value and reference types play a complementary role in modeling real-world domains and designing applications.

Copyright © Khawer Khaliq, 2017-25. All rights reserved.