Skip to content

Generating `Hashable` and `Equatable` for functions

Posted on:April 4, 2023

Some time ago, before I decided to leave Twitter permanently, I shared this snippet of code for generating Hashable and Equatable implementations for a struct that has a type that cannot by definition be Hashable or Equatable, such as a stored function in a variable.

As an example, let’s say we have a simple ViewModel struct that is passed an action function:

struct ViewModel: Equatable {
    let id: String
    let name: String
    let action: () -> Void // This property causes an error
}

Normally, we’d have to create our own equality function for this struct, as the compiler isn’t sure what to do with the action property. The function we’d write would omit the action from the Equality calculation. Similarly, if this struct was Hashable, we would also have to do it for the hash function. But what if we could convince the compiler to do it for us instead?

Using property wrappers, we can make this easy!

@propertyWrapper
public struct IgnoreEquatable<Wrapped>: Equatable {
    public var wrappedValue: Wrapped

    public static func == (
        lhs: IgnoreEquatable<Wrapped>,
        rhs: IgnoreEquatable<Wrapped>
    ) -> Bool {
        true
    }

    public init(wrappedValue: Wrapped) {
        self.wrappedValue = wrappedValue
    }
}

@propertyWrapper
public struct IgnoreHashable<Wrapped>: Hashable {
    @IgnoreEquatable public var wrappedValue: Wrapped

    public func hash(into hasher: inout Hasher) {}

    public init(wrappedValue: Wrapped) {
        self.wrappedValue = wrappedValue
    }
}

Now, instead of writing our ViewModel with a hand-written equality function, we can just write it like so:

struct ViewModel: Equatable {
    let id: String
    let name: String
    @IgnoreEquatable var action: () -> Void // Note the change from `var` to `let`
}

And if we instead wanted it to be Hashable, we could just use the @IgnoreHashable wrapper:

// Hashable requires Equatable, but will automatically generate it for you if possible
struct ViewModel: Hashable {
let id: String
    let name: String
    // Note that @IgnoreHashable wraps its value with `@IgnoreEquatable`. Writing `@IgnoreHashable @IgnoreEquatable` would be incorrect.
    @IgnoreHashable var action: () -> Void
}

Updated 04/19/2023

A previous version of this article did not update the declaration of action to be a var and left them as let. This error was pointed out by reader Parthasarathy Gudivada – thank you for the correction!