You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Have you ever needed to compare two arrays in Swift, but the order of elements doesn't matter? I find this often happens to me when convert between an ordered type such as `Array` and an unordered type such as `Set` or `Dictionary`. Today, we'll explore how to implement **unordered equality checking** in Swift, starting with the basics and working our way up to a flexible, protocol-based solution.
12
13
13
-
## What is Deep Equality
14
+
## What is Deep Equality?
15
+
16
+
Before diving into unordered equality, let's briefly discuss deep equality. Many programming languages struggle with deep equality comparisons. Take JavaScript, for example:
17
+
18
+
```javascript
19
+
consta1=1
20
+
consta2=1
21
+
console.log(a1 === a2) // true
22
+
23
+
constobj1= { a:1, b: { c:2 } }
24
+
constobj2= { a:1, b: { c:2 } }
25
+
26
+
console.log(obj1 === obj2) // false
27
+
console.log(obj1.b===obj2.b) // false
28
+
console.log(obj1.b.c===obj2.b.c) // true
29
+
```
30
+
In this example, we have two reference types `obj1` and `obj2`. They have the exact same structure so from a value perspective we could say that they are "equal".
31
+
32
+
JavaScript's `===` operator only checks reference equality for objects, **not their contents**. In other words, JavaScript does not check for **deep equality**. If you'd like to check for deep equality, you'd need to manually implement it yourself or use a library like Lodash's `_.isEqual()`.
14
33
15
34
## Swift Equality is Deep By Default
16
35
17
-
## Sometimes We Don't Need True Deep Equality
36
+
Swift, on the other hand, provides deep equality out of the box through the `Equatable` protocol. When you compare two values using `==`, Swift automatically performs a deep comparison of all their properties:
37
+
38
+
```swift
39
+
structPerson: Equatable{
40
+
let name: String
41
+
let age: Int
42
+
let address: Address
43
+
}
44
+
45
+
structAddress: Equatable{
46
+
let street: String
47
+
let city: String
48
+
}
49
+
50
+
let person1 =Person(name: "John", age: 30, address: Address(street: "123 Main St", city: "Boston"))
51
+
let person2 =Person(name: "John", age: 30, address: Address(street: "123 Main St", city: "Boston"))
52
+
53
+
print(person1 == person2) // true
54
+
```
55
+
Of course, that is assuming that each type and all their nested types correctly implemented `Equatable`. If you have any custom types with an incorrect implementation of `Equatable` then that incorrect implementation will bubble up to every type that holds it as a property. Thankfully, for value types, Swift implements `Equatable` automatically for us, so we get automatically correct deep equality checking for most types!
56
+
57
+
## Sometimes We Don't Want True Deep Equality
58
+
59
+
While deep equality is great, sometimes we need something different. Consider a scenario where you're working with collections and you care about *what* elements are present, but not *where* they appear. For example:
60
+
61
+
- Checking if two arrays contain the same elements regardless of order
62
+
- Verifying that two sets of user permissions are equivalent
63
+
- Comparing two API responses to see if there has been any change.
18
64
19
65
## Unordered Equality Checking
20
66
21
-
Our requirements:
22
-
1. Equality: Accurately checks if two values are equal
23
-
2. Deep equality: all of their properties are equal, no matter how deeply nested
24
-
3. Unordered: the two values should still be considered "equal" even if they are in different orders
25
-
4. Frequency: the values should have the same frequency of elements
26
-
5. Instance Method: We want this function to be an instance method.
27
-
6. Easily reusable: We would like this method to be usable from many types without having to rewrite it.
67
+
Let's implement a solution that satisfies these requirements:
68
+
69
+
1.**Equality**: Accurately checks if two values are equal
70
+
2.**Deep equality**: all of their properties are equal, no matter how deeply nested
71
+
3.**Unordered**: the two values should still be considered "equal" even if they are in different orders
72
+
4.**Frequency**: the values should have the same frequency of elements (for example, if `array1` has three `"A"` strings, then so should `array2`)
73
+
5.**Instance Method**: We want this function to be an instance method, usable directly from the collection type.
74
+
6.**Easily reusable**: We would like this method to be **usable from many types** without having to rewrite it
28
75
29
76
### Our Dream Call Site
30
-
I find it is often helpful to start with what you would like the call site to look like. Then figure out how you would implement that.
77
+
78
+
Let's start with what we want our API to look like:
31
79
32
80
```swift
33
81
let array1 = [1, 2, 3, 3]
34
82
let array2 = [3, 3, 2, 1]
35
-
array1.hasSameElements(as: array2) // true
83
+
array1.hasSameElements(as: array2) // true
36
84
```
37
85
38
-
Our function should evaluate to true if and only if:
39
-
1. The same elements are present in both collections.
40
-
2. No element is present in one collection but not the other.
41
-
3. Each collection has the same amount of occurences of each element.
42
-
4. The function should still be true if each collection is in a different order.
86
+
This should return `true` if and only if:
87
+
1. The same elements are present in both collections
88
+
2. No element is present in one collection but not the other
89
+
3. Each collection has the same number of occurrences of each element
90
+
4. The function should still return true if each collection is in a different order
43
91
44
92
## Concrete Method on Array
45
-
Let's start with adding a concrete method on `Array`. Later we can figure out how to generalize our solution to other types such as `Set`
We have a clear function definition. Now we need to figure out how to implement this. Remember we can't simply use `self == otherArray` because it would check both equality AND ordering. In other words we need to figure out how to count the frequency of each element in the array.
56
-
57
-
### Implementing `countFrequency()`
94
+
A good principle is don't start with generics. Instead, start with a concrete type, then generalize your code AFTER you get the concrete version working. Remember we can't simply use `self == otherArray` because it would check both equality AND ordering. In other words we need to figure out how to count the frequency of each element in the array. Let's start by implementing this functionality specifically for arrays:
58
95
59
96
```swift
60
97
extensionArraywhereElement:Hashable {
@@ -65,18 +102,7 @@ extension Array where Element: Hashable {
65
102
}
66
103
return result
67
104
}
68
-
}
69
-
```
70
-
71
-
#### Hashable Requirement
72
-
Try removing `where Element: Hashable`. You'll see that our method will fail to compile. This is because we are using a `Dictionary` and `Dictionary` requires that all of its keys conform to `Hashable`.
73
-
74
-
### Implementing `hasSameElements(as:)`
75
-
76
-
Now that we have `countsFrequency()` we can finish implementing `hasSameElements(as:)`.
@@ -85,40 +111,46 @@ extension Array where Element: Hashable {
85
111
}
86
112
```
87
113
114
+
First we will count the number of occurances of each unique value in the `Array` with our new `countFrequency` method. Here we simply create a `Dictionary` and store each element in the dictionary. Every time we find a new value we will increment up the count by one. Now that we've implemented counting the frequency, we can simply compare the frequency of both arrays using another new method: `hasSameElements(as:)`.
115
+
116
+
### The Hashable Requirement
117
+
118
+
Notice the `where Element: Hashable` constraint. This is crucial because we're using a `Dictionary` to count frequencies, and dictionary keys must be `Hashable`. If you remove this constraint, the code won't compile. This is an unfortunate extra contraint but it's more than a fair tradeoff in this case. The `Hashable` protocol in Swift inherits from `Equatable`, meaning every `Hashable` type is `Equatable` and most `Equatable` types are `Hashable`. Most types can let the compiler automatically synthesize `Hashable` conformance for them.
119
+
88
120
## Generalizing Our Solution
121
+
89
122
Now that we figured out how to add this method to `Array`. What we want to do is add this method to any type that might be able to benefit from this functionality. In OOP languages we would do this by adding it as a method on a superclass. Then every subclass would automatically inherit the new method. This is a fine strategy, but Swift offers another approach: protocol inheritance.
90
123
91
124
Protocols offer a few benefits. A type can inherit multiple protocols, unlike classes. Also, value types like `structs` and `enums` can also conform to and inherit protocols. So let's not fight against the language. Let's work with the strategy that is used throughout the standard library and ecosystem. Let's add our method to a protocol.
92
125
93
-
The problem is... which protocol should we extend? Answering this question is a regular painpoint for me. Swift protocols are wonderful because they are simple, self-contained, and very composable. Unfortunately that also means that they have a very very complex web of inheritance hierarchies.
126
+
The problem is... which protocol should we extend? Answering this question is a regular painpoint for me. Swift protocols are wonderful because they are simple, self-contained, and very composable. Unfortunately that also means that they have a [very very complex web of inheritance hierarchies](https://swiftdoc.org/v4.2/protocol/sequence/hierarchy/).
94
127
95
-
After much head scratching, I eventually settled on this:
128
+
After much head scratching, I eventually settled on this:
96
129
97
130
```swift
98
131
extensionSequencewhereElement:Hashable {
99
-
///count the amount of unique occurences of each value in a type
132
+
///Count the number of occurrences of each value in a sequence
100
133
publicfunccountFrequency() -> [Element: Int] {
101
134
var result = [Element:Int]()
102
135
for element inself {
103
136
result[element, default: 0] +=1
104
137
}
105
138
return result
106
139
}
107
-
}
108
-
109
-
extensionSequencewhereElement:Hashable {
110
-
/// Check for collection equality while ignoring order
111
-
publicfunchasSameElements(asc2: Self) ->Bool {
140
+
141
+
/// Check for sequence equality while ignoring order
142
+
publicfunchasSameElements(ass2: Self) ->Bool {
112
143
let freq1 =self.countFrequency()
113
-
let freq2 =c2.countFrequency()
144
+
let freq2 =s2.countFrequency()
114
145
return freq1 == freq2
115
146
}
116
147
}
117
148
```
118
149
119
-
I chose `Sequence` because it is the most fundamental protocol for our use case. It doesn't inherit from any other protocol. Practically all of the types that hold multiple values inherit from `Sequence`. For example `Array`, `Set`, `Dictionary`, `Range` etc.
150
+
I chose `Sequence` because it is the most fundamental protocol for our use case. It doesn't inherit from any other protocol. Practically all of the types that hold multiple values inherit from `Sequence`. For example `Array`, `Set`, `Dictionary`, `Range` etc.
151
+
152
+
### Performance Optimization
120
153
121
-
### Optimization
122
154
This function meets our requirements nicely. It's easy to read, and it is reusable in so many types and situations. However, perhaps we could improve it's performance a little. Currently we must iterate through both sequences entirely. That's two `O(n)` iterations back to back.
123
155
124
156
But what if they have different counts? Then we already know that the answer should be `false`. All of that work is unnecessary. Why don't we read the `count`, and escape early if the `count` is unequal? Now add this:
@@ -137,9 +169,46 @@ extension Collection where Element: Hashable {
137
169
138
170
`Sequence` has no `count` property, but `Collection` does. Now we have two implementations of the same method. Remember `Collection` inherits from `Sequence`. This means that if a `Collection` type calls this method it will use the `Collection` implementation, NOT the `Sequence` implementation. And that's great because the `Collection` implementation is more efficient!
139
171
172
+
## Real-World Usage
173
+
174
+
Here are some practical examples of where you might use this functionality:
By leveraging Swift's protocol-oriented programming and type system, we've created a flexible, reusable solution for unordered equality checking. Our implementation:
201
+
202
+
- Works with any `Sequence` whose elements are `Hashable`
203
+
- Provides optimized performance for `Collection` types
204
+
- Maintains type safety through protocol constraints
205
+
- Is easy to use and understand
206
+
207
+
This approach demonstrates the power of Swift's protocol system and shows how we can create elegant, reusable solutions to common programming challenges. If you like this approach, then grab the code for yourself [here](https://gist.github.com/DandyLyons/8ab7e104c25a9ed3d3a58967de1fb037).
208
+
209
+
Next week we'll continue our series on equatability in Swift, learning how to selectively check equality on just the properties we care about.
210
+
142
211
## Recommended Reading
143
212
-[Swift by Sundell | The different categories of Swift protocols](https://www.swiftbysundell.com/articles/different-categories-of-swift-protocols)
144
213
-[Swift forums | Is there any easy way to see the entire protocol hierarchy of ...](https://forums.swift.org/t/is-there-any-easy-way-to-see-the-entire-protocol-hierarchy-of-something-like-array-or-double/49193)
145
-
-[Swiftdoc.org | Sequence hierarchy graph (outdated Swift 4.2)](https://swiftdoc.org/v4.2/protocol/sequence/hierarchy/): (Unfortunately this is the latest I could find.)
214
+
-[Swiftdoc.org | Sequence hierarchy graph (outdated Swift 4.2)](https://swiftdoc.org/v4.2/protocol/sequence/hierarchy/): (Unfortunately this is the latest I could find. Please let me know if you find something more up to date.)
0 commit comments