Published on

Non-equatable void

Authors

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 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 :)
}