Scientific Witchery
2 min

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