• Skip to main content
  • Skip to footer

Khawer Khaliq

  • Home

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

Share
Tweet
Share
Pin

This article describes the Equatable and Identifiable protocols, and how they can enable Swift programmers to build domain models largely, if not entirely, using value types. While Equatable facilitates using value types not only to model simple values but also to create composite values rich in the language and functionality of the domain, Identifiable makes it possible to use value types to model domain entities with a stable identity. By choosing an appropriate identifier to use with the Identifiable protocol, the duration and scope of the identity can be controlled according to the requirements of the application.

Related articles:

  • When and How to Use Value and Reference Types in Swift
  • Encapsulating Domain Data, Logic and Business Rules With Value Types in Swift

Contents

The Equatable protocol
When to conform to Equatable
Modeling values using Swift value types
The Identifiable protocol
When to conform to Identifiable
Modeling entities with identity using Swift value types
Conclusion

The Equatable protocol

The Equatable protocol enables comparison of instances of the same type for value equality. Instances of a type that conforms to Equatable can be compared for value equality using the == operator, and for value inequality using the != operator. Most basic types in the standard library conform to Equatable and a number of other types rely on Equatable conformance. For instance, the contains(_:) method in the Sequence protocol, which returns true if the sequence contains the given element and false otherwise, is available only if elements of the sequence conform to Equatable. Similarly, the firstIndex(of:) method of the Array type is only available if elements of the array conform to Equatable.

The Equatable protocol has the following requirements:

static func ==(Self, Self) -> Bool
static func !=(Self, Self) -> Bool

In practice, only an implementation of the == operator needs to be provided since the standard library contains an implementation of the != operator for Equatable types, which negates the result of the == operator. Things are made even easier for structs and enums, which are the main value types in Swift. Enums without associated values automatically conform to Equatable. Structs, and enums with associated values, can opt in to automatic synthesis of Equatable conformance, provided:

  • For structs, all stored properties are of types that conform to Equatable, and Equatable conformance is declared in the original definition of the struct or in an extension in the same file
  • For enums with associated values, all associated values are of types that conform to Equatable, and Equatable conformance is declared in the original definition of the enum or in an extension in the same file

If a struct or enum does not meet the above requirements, or it is considered desirable to customize the Equatable conformance for some reason, an implementation of the == operator must be provided when declaring Equatable conformance.

Given below is an example of a simple type representing a box, implemented as a struct, which gets Equatable conformance just by declaring it in the definition of the struct.

struct Box: Equatable {
    var length: Double
    var width: Double
    var height: Double
}

let box = Box(length: 10, width: 10, height: 5)
let otherBox = Box(length: 10, width: 8, height: 7)

print(box == otherBox)  // false
print(box != otherBox)  // true

Here is a simple enum example, where there is no need to explicitly declare Equatable conformance since there are no associated values.

enum Direction {
    case north, south, east, west
}

let direction = Direction.north
let otherDirection = Direction.east

print(direction == otherDirection)  // false
print(direction != otherDirection)  // true

Now consider the following struct that models a point in a two-dimensional plane.

struct Point {
    var x: Double
    var y: Double
}

It is natural to expect to be able to compare points for equality using their x and y coordinates. Unfortunately, instances of the Point type, as defined above, cannot be used with the == or != operators even though both the stored properties are of the Double type, which is Equatable, because Equatable conformance was not declared in the definition of the Point struct. This can easily be remedied by modifying the original definition of Point to opt in to automatic synthesis of Equatable conformance. Alternatively, an extension can be used to opt in to automatic synthesis of Equatable conformance, provided the extension is in the same file as the original type definition.

// Extension in the same file as the original struct
extension Point: Equatable {}

If the two options presented above are not available, Equatable conformance may still be added through an extension in a separate file by providing an implementation of the == operator required by the Equatable protocol. However, when declaring Equatable conformance through an extension in a separate file, access levels assigned to the type’s members needs to be considered. Any private or fileprivate members cannot be accessed by an extension in a separate file. Members with the internal access level, which is the default access level in Swift, cannot be accessed by an extension in a file not in the same module as the original type definition.

The extension shown below can, therefore, be in a separate file provided the file is in the same module as the original struct, since both the stored properties of Point have the default internal access level.

// Extension in a separate file in the same module as the original struct
extension Point: Equatable {
    static func ==(lhs: Point, rhs: Point) -> Bool {
        lhs.x == rhs.x && lhs.y == rhs.y
    }
}

It is noteworthy that classes do not benefit from automatically synthesized Equatable conformance and, if a class type is to be made Equatable, the conformance must always be manually implemented. This is because classes are reference types, with each instance having an identity unrelated to the value of its attributes. Therefore, it is up to the designer of a class to decide why Equatable conformance is desirable for a certain type implemented using a class and how best to implement Equatable conformance in this context.

When to conform to Equatable

All types that model values should conform to the Equatable protocol. The most common values we know from everyday experience are numbers. But there are many other values in the domains programmers work with and it is important to be able to identify such values so they can modeled appropriately.

Listed below are some defining characteristics of values:

  1. Lack of identity: A value does not have inherent identity and is defined only by the values of its attributes.
  2. Attribute-based equality: Any two values of the same type whose corresponding attributes are equal are considered equal.
  3. Substitutability: Any value can be freely substituted for another value provided the two are equal , i.e., they are of the same type and their corresponding attributes are equal.
  4. Immutability: Once created, a value cannot be changed. An operation on a value may produce a new value of the same type, which if saved in the same variable where the original value was saved, can create an illusion of mutability. A value saved in a constant (declared using the let keyword in Swift) thus provides a guarantee of immutability of its attributes.

Let’s look at the Box type we had defined earlier and see if it passes all the above tests. An instance of the Box type is defined only in terms of its dimensions (length, width and height) and has no identity independent of these attributes. Any two Box instances that have the same dimensions (attributes) are considered equal. When we need a Box instance, we can use any instance with the required dimensions. It follows that any Box instance can be substituted for another provided the corresponding dimensions (attributes) of the two instances are equal.

The concept of immutability of values can sometimes be confusing in Swift. It may seem at first that a Box instance is mutable as it has var properties. This is not true, however. Whenever we modify any property of a given Box instance, under the hood, Swift creates a new instance with the new property values and replaces the original instance with the new instance, creating the aforementioned illusion of mutability (this also happens when we call a mutating method on an instance of a struct or enum).

A Box instance assigned to a constant, therefore, provides a guarantee of immutability of its stored properties, as it is not possible to assign a new value to constant, as shown below.

let myBox = Box(length: 20, width: 10, height: 5)
myBox.length = 10
// Error: Cannot assign to property: 'myBox' is a 'let' constant

Modeling values using Swift value types

Swift comes with a rich complement of value and reference types. Classes and closures are reference types while structs, enums and tuples are value types. Structs and classes are the most commonly used value and reference types respectively. Swift structs are particularly powerful, with the ability to define properties and methods, specify initializers, and conform to protocols. Combining structs and protocols enables abstraction and polymorphism without the need for inheritance.

Once we identify the values in our domain, we can model them using Swift value types, usually structs. This solves a major challenge faced by object-oriented programmers of having to use classes, which are reference types, to model values. Using classes where they are not fit for purpose not only makes the code unnecessarily inefficient but also creates potential for various types of logic errors and makes parts of the code harder to reason about.

Correctly identifying the values in a domain and modeling them using value types leads to the following advantages:

  • Value types make a program more efficient because instances of value types are easy to create, copy and destroy, without the overhead of heap memory allocation and reference counting that comes with creating and managing instances of reference types
  • Code dealing with value types is more predictable and easier to reason about because value types provide strong guarantees about mutability, leading to more fine-grained control over how certain parts of the code will behave
  • Using value types makes it easier to write multi-threaded code, without the need for complex synchronization mechanisms, because a thread can use a value type instance without worrying about the possibility of another thread mutating its state
  • Since value types have no references, they eliminate the possibility of memory leaks caused by strong reference cycles
  • Code written using value types is generally more easily testable as all we usually need to do is to create a new value with the expected attributes and test it for equality against the value produced by the program, reducing the need for elaborate setup code and mocking frameworks

The Equatable protocol plays an important role in facilitating the use of value types to model values by ensuring that we can compare instances of the same type for equality based on the values of their attributes. This is why Swift makes it so easy to make structs and enums conform to Equatable, by automatically synthesizing this conformance in most cases.

The Identifiable protocol

The Identifable protocol is for use with any type that needs to have a stable notion of identity. It is declared as follows.

protocol Identifiable {
    associatedtype ID : Hashable
    var id: ID { get }
}

The only requirement of the protocol is that conforming types have a property called id. The protocol leaves it to conforming types to decide what type they want to use for the id property, provided it conforms to the Hashable protocol. This gives flexibility to conforming types to decide whether to use a domain-specific unique identifier or to create a unique identifier within the application with an appropriate duration and scope of the identity.

When to conform to Identifiable

Real-world domains contain not only values but also entities that have a stable identity independent of the values of their attributes. Values are easily dealt with using Swift value types and Equatable conformance. This approach does not work, however, for entities in the domain where we need to preserve their identity. Traditionally, classes have been used to model such entities, which meant having to live with all the issues related to using reference types. With the introduction of the Identifiable protocol, however, Swift value types can be used to model entities with identity. Identifiable conformance is helpful not only in marking out such types but also in providing a uniform manner in which instances of conforming types can be compared using their unique identifiers.

Many domains have existing ways to uniquely identify instances of entities that are suitable for the purposes of the domain model, with identifiers such as part number, product code, customer code, bank account number, etc. In such cases, we can usually use the appropriate domain-specific identifier, stored as a string, as shown below.

struct Part: Identifiable {
    let id: String
    var description: String
}

var part = Part(id: "101C", description: "Bolt, steel, 3.0cm")

In the absence of an existing system of unique identifiers for an entity, we need to create unique identifiers within the application. One simple way to do this is to use integers as identifiers, making the integers auto-incrementing to make them unique.

struct Employee: Identifiable {
    private static var nextID = 0
    
    let id: Int
    var name: String
    
    init(name: String) {
        Employee.nextID += 1
        id = Employee.nextID
        self.name = name
    }
}

var firstEmployee = Employee(name: "Jane Smith")
var secondEmployee = Employee(name: "Oliver Jones")

print(firstEmployee.id)     // 1
print(secondEmployee.id)    // 2

While this approach can work for simple cases, for a single launch or even across launches where a persistent data store is used to coordinate identifiers between launches, it does not scale well, in particular when uniqueness is required across devices or by threads running concurrently.

Swift includes a solution for this out of the box with the UUID type, which provides universally unique identifiers. There are two ways to use the UUID type when conforming to the Identifiable protocol. We can directly use UUID as the type of the id property, or we can use the uuidString property of the UUID type to generate a unique string identifier, in which case the id property would be of the String type, as shown below.

let id = UUID().uuidString

Shown below is the Employee type rewritten with UUID as the type of the id property.

struct Employee: Identifiable {
    let id = UUID()
    var name: String
}

var firstEmployee = Employee(name: "Jane Smith")
var secondEmployee = Employee(name: "Oliver Jones")

print(firstEmployee.id)     // 264FE82B-012B-4CEB-A2F8-D06EF94588E2
print(secondEmployee.id)    // 1A82D5E0-9F6C-4ADD-9889-BE3C616F64AD

Modeling entities with identity using Swift value types

Armed with the Identifiable protocol, we can use value types to model domain entities with a stable identity, getting all the benefits associated with value types mentioned earlier in this article. This significantly reduces, even eliminates in many cases, the need to use classes in the domain model. Consider a case where we need to model a student. To keep the example simple, we assume we just need to store a unique enrolment number, name, and GPA (Grade Point Average) for each student.

For purposes of illustration, let us first see how we run into issues if we attempt to model a student using a struct conforming to Equatable.

struct Student: Equatable {
    let enrolmentNumber: String
    var name: String
    var gpa: Double
}

We create an array containing two instances of the above type.

var students = [
    Student(enrolmentNumber: "1001", name: "Alice Cooper", gpa: 3.2),
    Student(enrolmentNumber: "1002", name: "James Smith", gpa: 3.0)]

Let’s say, on a periodic basis, we need to update the GPAs of certain students, and this is done by copying the student records from a central data repository, modifying the copied records as required, and then replacing the updated records in the repository. To demonstrate this for one student, we copy the second element of the array to a new variable, and change its gpa property.

var student = students[1]
student.gpa = 2.8

We then attempt to use the following code to find the relevant element in the students array and replace it with the value in the student variable.

if let index = students.firstIndex(of: student) {
    students[index] = student
    print("Record updated")
} else {
    print("Record not found")
}
// Record not found

print(students[1].gpa)  // 3.0

This clearly does not work. Since the Student struct we have used conforms to Equatable, each Student instance is defined only by its attributes. The moment we change the value of the gpa property, the new instance can no longer be related to the original instance because the firstIndex(of:) method will only find a matching element in the array if the values of all stored properties are equal. This is why we get a message that no matching record was found in the array. To confirm this, when we print the gpa property of the second element of the array, we find that the value has not been updated.

The same thing happens if we try to check whether the array contains a record for this student. Since the contains(_:) method also uses value-based equality to try and find a matching element in the array, we see that the student instance we modified could not be found in the array.

print(students.contains(student))   // false

The issue is that student is an entity with identity and should not be modeled using a struct conforming to Equatable. We, therefore, modify the Student type to make it conform to the Identifiable protocol. Since each student can be uniquely identified by their enrolment number, we use it as the identifier required by Identifiable.

struct Student: Identifiable {
    let id: String                 // Enrolment number
    var name: String
    var gpa: Double
}

As we had done before, we create an array containing two instances of the above type.

var students = [
    Student(id: "1001", name: "Alice Cooper", gpa: 3.2),
    Student(id: "1002", name: "James Smith", gpa: 3.0)]

Continuing to follow what we had done in our original example, we copy the second element of the array to a new variable, and update the gpa property of the copied instance.

var student = students[1]
student.gpa = 2.8

Now that Student is no longer Equatable, we can’t use the firstIndex(of:) method on the students array. With Identifiable conformance, however, we know that we can use the id property to find matching elements in the array. To do this, we invoke the firstIndex(where:) method on students, using the closure to compare the values of the id property to find a matching element. We rewrite the code to find the correct element in the array, and replace it with the updated value.

if let index = (students.firstIndex() { $0.id == student.id }) {
    students[index] = student
    print("Record updated")
} else {
    print("Record not found")
}
// Record updated

print(students[1].gpa)  // 2.8

This time we get a message that a matching record was found and updated. When we print the gpa value of the second element of the array, we see that the value is as expected.

Since we know that the students array contains elements that conform to Identifiable, we don’t need to perform the id comparison each time we have to find an index at which an element with a given id value may exist. Instead, we can rely on the assurance that every type conforming to Identifiable must have an id property, which conforms to Hashable so must be Equatable. Accordingly, we create an extension to Array to add a firstIndex(of:) method available only when elements of the array conform to Identifiable, which uses an equality check on the id property to find the index of the first matching element, if one exists.

extension Array where Element: Identifiable {
    func firstIndex(of element: Element) -> Int? {
        firstIndex() { $0.id == element.id }
    }
}

We can now rewrite the code we wrote earlier to use the above method, which makes it simpler and more intuitive.

if let index = students.firstIndex(of: student) {
    students[index] = student
    print("Record updated")
} else {
    print("Record not found")
}
// Record updated

print(students[1].gpa)  // 2.8

Similarly, we can create the following extension to the Sequence protocol, which is available only when elements of the sequence conform to Identifiable. This extension defines a contains(_:) method that checks if the sequence contains an element using an equality comparison on the values of the id property.

extension Sequence where Element: Identifiable {
    func contains(_ element: Element) -> Bool {
        contains() { $0.id == element.id }
    }
}

Using this method, we can confirm that the students array contains an element with the same id value as the instance assigned to the student variable, in a simpler and more intuitive way.

print(students.contains(student))   // true

Conclusion

Swift value types are ideally suited to modeling values, by conforming to the Equatable protocol. With the introduction of the Identifiable protocol, however, value types in Swift can also be used to model entities with a stable identity, while controlling the duration and scope of the identity according to the needs of the application. This means that domain models can be built largely, or even entirely, without using classes, which facilitates writing code that is more efficient, easier to reason about, simpler to test, and more concurrency-friendly.

Thank you for reading! I always appreciate constructive comments and feedback. Please feel free to leave a comment in the comments section of this post or start a conversation on Twitter.

Subscribe to get notifications of new posts

No spam. Unsubscribe any time.

Reader Interactions

Comments

  1. Remco Poelstra says

    January 22, 2022 at 7:40 pm

    Hi,

    Thanks for this great post. I’ve a question though:
    You mention that conforming to `Identifiable` reduces the need for classes. But what if you want to couple the `Student` to e.g. a `Teacher`? You can store the ID of the `Teacher`, but that doesn’t get you the teacher itself. So how would this work in a bit larger context?
    Thanks in advance.

    Reply
    • Khawer Khaliq says

      January 26, 2022 at 1:25 pm

      Hi Remco,

      Thanks for your comment. Glad that you like the article.

      Classes have traditionally been used to model domain entities with a stable identity independent of the values of their attributes. These would include entities such as a student or, as you have noted, a teacher. As with the Student type in the example used in the article, we can create a Teacher type as a struct conforming to Identifiable, which means it will have an id property conforming to the Hashable protocol. Since we know Teacher conforms to Identifiable, we can use the id property to find a teacher who may be linked to a particular student. We still need to use classes where we need stable storage, to create a view model to support a SwiftUI view for instance. But with the Identifiable protocol, we no longer need to use classes to model every domain entity that has a stable identity. This, as noted in the article, reduces the need to use classes in our applications, letting us leverage the benefits of using value types.

      Hope this answers your question. Happy to elaborate more if required, or to help model a specific scenario if you have one in mind.

      Reply
  2. luqmaan s says

    September 6, 2023 at 7:36 am

    Great explanation of Equatable and Identifiable in Swift! Understanding when to use them for value types and entities makes Swift programming more efficient and robust. Thanks for the insights!

    Reply
    • Khawer Khaliq says

      September 10, 2023 at 10:08 am

      Hi Luqmaan,

      Thanks for your comment, and happy to see that you found the article useful.

      Cheers,
      Khawer

      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.

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.

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

Uses type erasure to implement Equatable conformance at the protocol level, allowing us to program to abstractions using protocol types while safely making equality comparisons and using functionality provided by the Swift Standard Library only available to types that conform to Equatable.

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