Non-equatable void
A workaround for non-nominal types that don’t conform to Equatable, but really should
In Swift, we can use the Equatable protocol to compare two values for equality. For example, we can compare two Strings:
let a = "Hello"
let b = "Hello"
a == b // true
We can also compare two of any struct or class that conforms to Equatable:
struct Person: Equatable {
let name: String
let age: Int
}
let a = Person(name: "John", age: 42)
let b = Person(name: "John", age: 42)
a == b // true
Additionally, many types conform to equatable based on conditional generic conformances, such as optional:
let a: String? = "Hello"
let b: String? = "Hello"
a == b // true
In Swift, though, non-nominal types (that is, types that don't have a name) cannot conform to protocols. There are a few non-nominal types that come to mind:
- Functions, such as
(Int) -> Void - Closures, such as
{ (Int) -> Void in } - Tuples, such as
(Int, String)Void(uh oh)
- Metatypes, such as
Int.Type - Existentials, such as
Sendable & AnyObject
The most commonly used type here is the tuple type. These are usually used as shorthand for a plain data structure, and it can be surprising that they don't conform to Equatable:
let a = (1, "Hello")
let b = (1, "Hello")
a == b // error: binary operator '==' cannot be applied to two '(Int, String)' operands
Void falls into this category. The definition of void in swift is typealias Void = (), so it's a tuple with no elements. You can compare two voids, but using Void in a struct will prevent automatic equatable conformance:
struct Person: Equatable { // error: type 'Person' does not conform to protocol 'Equatable'
let name: String
let age: Int
let void: Void
}
You can get around this by implementing == yourself, but that's a lot of boilerplate for a simple struct.
struct Person {
let name: String
let age: Int
let void: Void
}
extension Person: Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.name == rhs.name && lhs.age == rhs.age
// Can also put
// && lhs.void == rhs.void
// and it will work, but it's not necessary
// just unexpected that it won't autogenerated, but the definition of an equality operator doesn't automatically create equatable conformance
}
}
So, there are a couple of options at this point
- Create a variant of every generic type that you want to use with
Voidthat removes the void field. This is a lot of boilerplate. - Create a nominal type that has the same effect as void but conforms to most protocols. An example (or so copilot tells me) would be something like
struct Void: Equatable, Hashable, Codable, ExpressibleByNilLiteral {
init(nilLiteral: ()) {}
}
- Craft a type that acts like Void, but isn't void. The type I use is
Never?, which can only have one value,nil.
There's probably a way to do this. Please let me know!
struct Person: Equatable {
let name: String
let age: Int
let void: Never? // works :)
}