- Published on
Non-equatable void
- Authors
- Name
- Jack Youstra
- @jackyoustra
In Swift, we can use the Equatable
protocol to compare two values for equality. For example, we can compare two String
s:
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
Void
that 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 :)
}