Swift - Making a struct hashable that has a property of protocol type

479 views Asked by At

I have a struct that needs to be Decodable and Hashable. This struct has a property that is of a Protocol type. Depending on the type a concrete value of the protocol is filled in the struct. But how do I make this struct hashable without making the protocol Hashable (which makes it a Generic protocol which can't be directly used)?

enum Role: String, Decodable, Hashable {
 case developer
 case manager
  ....
}

protocol Employee {
 var name: String { get }
 var jobTitle: String { get }
 var role: Role { get }
}

struct Manager: Employee, Hashable {
 let name: String
 let jobTitle: String
 let role: Role = .manager
  ....
}

struct Developer: Employee, Hashable {
 let name: String
 let jobTitle: String
 let role: Role = .developer
 let manager: Employee  // Here is the problem
 
 static func == (lhs: Developer, rhs: Developer) -> Bool {
  lhs.name == rhs.name &&
  lhs.jobTitle == rhs.jobTitle &&
  lhs.role == rhs.role &&
  lhs.manager == rhs.manager // Type 'any Employee' cannot conform to 'Equatable'
 }
 
 func hash(into hasher: inout Hasher) {
  hasher.combine(name)
  hasher.combine(jobTitle)
  hasher.combine(role)
  hasher.combine(manager) // Instance method 'combine' requires that 'H' conform to 'Hashable'
 }
}

There are multiple issues with this:

  1. Just to make one property Hashable/Equatable we need to write the == and hash function with all the properties.
  2. Even though we do that, there is still a problem where the protocol is not Hashable/Equatable.

Is there some other or right way to do this?

1

There are 1 answers

3
Rob Napier On

IMO, you've done this inside-out. You have an enum that has a 1:1 correspondence to structs. That suggest the enum should contain the data rather than using a protocol:

indirect enum Employee: Decodable, Hashable {
    case developer(Developer)
    case manager(Manager)

    var name: String {
        switch self {
        case .developer(let developer): return developer.name
        case .manager(let manager): return manager.name
        }
    }
    var jobTitle: String {
        switch self {
        case .developer(let developer): return developer.jobTitle
        case .manager(let manager): return manager.jobTitle
        }
    }
}

struct Manager: Decodable, Hashable {
    let name: String
    let jobTitle: String
}

struct Developer: Decodable, Hashable {
    let name: String
    let jobTitle: String
    let manager: Employee
}

This is slightly tedious because every property needs a switch over every case. If there are limited number of properties, that's fine, but it can be annoying if the list of properties is large and grows.

Since Employees are all quite similar, another approach is to just put what varies into their role. (I'm assuming this is all a generic example, since in most organizations, managers also have managers. This may be a time to rethink your types and decide if Managers and Developers are actually different things. If developer.manager can be a Developer, why two types?)

indirect enum Role: Decodable, Hashable {
    case developer(Developer)
    case manager(Manager)
}

struct Employee: Decodable, Hashable {
    // all the same things
    var name: String
    var jobTitle: String

    // a bundle of all the special things
    var role: Role
}

struct Manager: Decodable, Hashable {}

struct Developer: Decodable, Hashable {
    let manager: Employee
}

But you can also keep your current design with a protocol. In your example, you don't need any Employee to be Equatable. You need to be able to compare an Employee to an arbitrary other Employee. That's not the same thing.

Extend your Employee this way, to allow it to be compared to arbitrary other Employees (which is not the same thing as Equatable, which only means a type can be compared to Self).

protocol Employee {
    var name: String { get }
    var jobTitle: String { get }
    var role: Role { get }
    func isEqual(to: any Employee) -> Bool
}

extension Employee where Self: Equatable {
    func isEqual(to other: any Employee) -> Bool {
        guard let other = other as? Self else {
            return false
        }
        return self == other
    }
}

With that, your == implementation looks like:

static func == (lhs: Developer, rhs: Developer) -> Bool {
    lhs.name == rhs.name &&
    lhs.jobTitle == rhs.jobTitle &&
    lhs.role == rhs.role &&
    lhs.manager.isEqual(to: rhs.manager)
}

The Hashable situation is even easier, because Hashable doesn't have an associated type of its own. So you can just include it as a requirement:

struct Developer: Employee, Hashable {
    ...
    let manager: any Employee & Hashable
    ...