Skip to content

Commit f34f605

Browse files
committed
Release post-23
1 parent d7d82a2 commit f34f605

File tree

2 files changed

+123
-53
lines changed

2 files changed

+123
-53
lines changed

content/en/posts/post-23/Unordered Equality Checking in Swift.md

Lines changed: 122 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,54 +7,91 @@ topics: ["Swift", "Equatable Protocol"]
77
images:
88
description:
99
---
10+
# Unordered Equality Checking in Swift
1011

11-
`Equatable` is a core feature of Swift.
12+
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.
1213

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+
const a1 = 1
20+
const a2 = 1
21+
console.log(a1 === a2) // true
22+
23+
const obj1 = { a: 1, b: { c: 2 } }
24+
const obj2 = { 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()`.
1433

1534
## Swift Equality is Deep By Default
1635

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+
struct Person: Equatable {
40+
let name: String
41+
let age: Int
42+
let address: Address
43+
}
44+
45+
struct Address: 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.
1864

1965
## Unordered Equality Checking
2066

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
2875

2976
### 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:
3179

3280
```swift
3381
let array1 = [1, 2, 3, 3]
3482
let array2 = [3, 3, 2, 1]
35-
array1.hasSameElements(as: array2) // true
83+
array1.hasSameElements(as: array2) // true
3684
```
3785

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
4391

4492
## 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`
46-
47-
```swift
48-
extension Array {
49-
public func hasSameElements(as otherArray: Self) -> Bool {
50-
// ???
51-
}
52-
}
53-
```
5493

55-
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:
5895

5996
```swift
6097
extension Array where Element: Hashable {
@@ -65,18 +102,7 @@ extension Array where Element: Hashable {
65102
}
66103
return result
67104
}
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:)`.
77-
78-
```swift
79-
extension Array where Element: Hashable {
105+
80106
public func hasSameElements(as otherArray: Self) -> Bool {
81107
let freq1 = self.countFrequency()
82108
let freq2 = otherArray.countFrequency()
@@ -85,40 +111,46 @@ extension Array where Element: Hashable {
85111
}
86112
```
87113

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+
88120
## Generalizing Our Solution
121+
89122
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.
90123

91124
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.
92125

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/).
94127

95-
After much head scratching, I eventually settled on this:
128+
After much head scratching, I eventually settled on this:
96129

97130
```swift
98131
extension Sequence where Element: 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
100133
public func countFrequency() -> [Element: Int] {
101134
var result = [Element: Int]()
102135
for element in self {
103136
result[element, default: 0] += 1
104137
}
105138
return result
106139
}
107-
}
108-
109-
extension Sequence where Element: Hashable {
110-
/// Check for collection equality while ignoring order
111-
public func hasSameElements(as c2: Self) -> Bool {
140+
141+
/// Check for sequence equality while ignoring order
142+
public func hasSameElements(as s2: Self) -> Bool {
112143
let freq1 = self.countFrequency()
113-
let freq2 = c2.countFrequency()
144+
let freq2 = s2.countFrequency()
114145
return freq1 == freq2
115146
}
116147
}
117148
```
118149

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
120153

121-
### Optimization
122154
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.
123155

124156
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 {
137169

138170
`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!
139171

172+
## Real-World Usage
173+
174+
Here are some practical examples of where you might use this functionality:
175+
176+
```swift
177+
// Comparing arrays of numbers
178+
let numbers1 = [1, 2, 3, 3]
179+
let numbers2 = [3, 1, 3, 2]
180+
print(numbers1.hasSameElements(as: numbers2)) // true
181+
182+
// Comparing sets of strings
183+
let set1: Set = ["apple", "banana", "orange"]
184+
let set2: Set = ["orange", "apple", "banana"]
185+
print(set1.hasSameElements(as: set2)) // true
186+
187+
// Working with custom types
188+
struct User: Hashable {
189+
let id: Int
190+
let name: String
191+
}
192+
193+
let users1 = [User(id: 1, name: "Alice"), User(id: 2, name: "Bob")]
194+
let users2 = [User(id: 2, name: "Bob"), User(id: 1, name: "Alice")]
195+
print(users1.hasSameElements(as: users2)) // true
196+
```
197+
140198
## Conclusion
141199

200+
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+
142211
## Recommended Reading
143212
- [Swift by Sundell | The different categories of Swift protocols](https://www.swiftbysundell.com/articles/different-categories-of-swift-protocols)
144213
- [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.)

content/en/posts/post-24/Selective Equality Checking in Swift.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ series: ["Swift Equatability"]
66
topics: ["Swift", "Equatable Protocol", "Parameter Packs"]
77
images:
88
description:
9+
draft: true
910
---
1011

1112
## Why Not Just Use Equatable

0 commit comments

Comments
 (0)