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
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
, andEquatable
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
, andEquatable
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:
- Lack of identity: A value does not have inherent identity and is defined only by the values of its attributes.
- Attribute-based equality: Any two values of the same type whose corresponding attributes are equal are considered equal.
- 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.
- 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.