0

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?

tarun_sharma
  • 761
  • 5
  • 22
  • If you want the struct to be equatable, everything it contains must be equatable too, there is no way around it. Did you consider using generics? – Sulthan Apr 22 '23 at 13:44
  • Yes. In this case, the `Employee` protocol needs to be Equatable/Hashable, which makes it a generic protocol. But then I can't use that in the Developer struct. I would need to use type erasure (AnyEmployee). If the Protocol has a further hierarchy, the whole thing swells and become unmanagable. – tarun_sharma Apr 22 '23 at 13:54
  • Did you consider using generics instead of protocols? – Sulthan Apr 22 '23 at 14:16
  • Could you not just omit the manager from the hash? – Fogmeister Apr 22 '23 at 14:17
  • why Developer.manager property is of type Employee? It sounds logical to change its type to Manager. That would solve those errors as well. – Olex May 03 '23 at 14:39

1 Answers1

1

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
    ...
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thanks, @Rob. This was an overly simplified made-up example to state the issue. The actual code is much more complex. Using the enum approach is a common and standard way which makes it a single value type with different values. But it loses the composition which makes it hard to scale. For ex. if you are using modules in the app, these modules are isolated from each other and works on different types of Employees. In that case, A base protocol can stay in the common module and then modules can extend the base protocol. They can create a concrete type or extended the base protocol. – tarun_sharma Apr 22 '23 at 15:48
  • With an enum that will not be possible because the cases with associated values need to be in the primary declaration. All the possible values of the enum will be exposed to all the modules which may not be desired. But the last suggestion (`any Employee & Hashable`) is interesting. I'll give it a try. – tarun_sharma Apr 22 '23 at 15:50
  • The `any Employee & Hashable` doesn't compile. It gives an error `Type 'Developer' does not conform to protocol 'Hashable'` – tarun_sharma Apr 22 '23 at 16:06