Is there a way to create a KeyPath to a property whose type conforms to a given protocol without a bunch of boilerplate?

So, for whatever reason, classes don't seem to be able to synthesize Equatable or Hashable conformances. I'm writing a bunch of large classes with a lot of properties, and I thought it'd be great if I could just give the compiler the key path(s) to the relevant property(s) for each class and compute equality from that.

So first I tried this:

protocol EquatableViaKeyPath : Equatable {
  static var keyPath: KeyPath<Self, Equatable> {get}
}

which of course gave me the "can only be used as a generic constraint" error WRT Equatable. Well, ok, I supposed I should've seen that one coming... Let's try a protocol that doesn't have those issues:

protocol EquatableViaKeyPath : Equatable {
  static var keyPath: KeyPath<Self, CustomStringConvertible > {get}
}
extension EquatableViaKeyPath {
  static func == (lhs: Self, rhs: Self) -> Bool {
    return lhs[keyPath: Self.keyPath].description == rhs[keyPath: Self.keyPath].description
  }
}

Well, that doesn't give me an error by itself... Let's try conforming a class to it:

class Foo : EquatableViaKeyPath { // Error: Type 'Foo' does not conform to protocol 'EquatableViaKeyPath' / Candidate has non-matching type 'ReferenceWritableKeyPath<Foo, Int>' 
  static var keyPath = \Foo.bar
  var bar: Int
  init(bar: Int) {
    self.bar = bar
  }
}

Hmm, that seems odd... ReferenceWritableKeyPath is a subclass of KeyPath so that part shouldn't be the problem, and CustomStringConvertible doesn't have any "Self or associated type requirements" so that part shouldn't be the problem. Maybe if I'm more explicit with the type?

class Foo : EquatableViaKeyPath {
  static let keyPath: KeyPath<Foo, CustomStringConvertible> = \Foo.bar
  var bar: Int
  init(bar: Int) {
    self.bar = bar
  }
}

Ah, now there are two errors: Key path value type 'Int' cannot be converted to contextual type 'CustomStringConvertible' and Protocol 'EquatableViaKeyPath' requirement 'keyPath' cannot be satisfied by a non-final class ('Foo') because it uses 'Self' in a non-parameter, non-result type position

So, the first one I'm thinking is maybe because it isn't generic enough? Let's change the protocol and class:

protocol EquatableViaKeyPath : Equatable {
  associatedtype Equatablebly: Equatable
  static var keyPath: KeyPath<Self, Equatablebly> {get}
}

class Foo : EquatableViaKeyPath {
  static let keyPath: KeyPath<Foo, Int> = \Foo.bar
  var bar: Int
  init(bar: Int) {
    self.bar = bar
  }
}

The error goes away. But if I get rid of the explicit type annotation in the class and just write:

  static let keyPath = \Foo.bar

I get an error about Foo not conforming to EquatableViaKeyPath and it offers to add the stubs, which turns out to be an explicit typealias for CSC. If I set that to be Int, it throws another error and the fix is to redeclare keyPath with an explicit type.

Ok, so apparently the explicit type is required, and I guess this works for when there's only one key path that needs to be considered, but what if there's more? Consider this version (which is what I actually want, other than the final part):

protocol EquatableViaKeyPath : Equatable {
  static var keyPaths: [KeyPath<Self, CustomStringConvertible>] {get}
}
extension EquatableViaKeyPath {
  static func == (lhs: Self, rhs: Self) -> Bool {
    return keyPaths.reduce(true) {
      $0 && lhs[keyPath: $1].description == rhs[keyPath: $1].description
    }
  }
}

final class Foo : EquatableViaKeyPath {
  static var keyPaths: [KeyPath<Foo, CustomStringConvertible>] = [\Foo.bar, \Foo.baz] // Error: Key path value type 'Int' cannot be converted to contextual type 'CustomStringConvertible'
  var bar: Int
  var baz: String
  init(bar: Int, baz: String) {
    self.bar = bar
    self.baz = baz
  }
}

Hmm... Oh, surely not...

final class Foo : EquatableViaKeyPath {
  static let keyPaths = [\Foo.barAsCSC, \Foo.bazAsCSC]
  var bar: Int
  var baz: String
  var barAsCSC: CustomStringConvertible { return bar as CustomStringConvertible }
  var bazAsCSC: CustomStringConvertible { return baz as CustomStringConvertible }
  init(bar: Int, baz: String) {
    self.bar = bar
    self.baz = baz
  }
}

No errors. *headdesk*

So, I have two questions.

  1. Does anyone know a way around the final requirement? It won't matter to my code if two different subclasses get compared for equality (the check would fail since some key data doesn't overlap), and some of them need to not be final.
  2. Does anyone know a way to work around or minimize these shenanigans with the *AsCSC computed properties? Can I somehow use reflection and/or the dynamic pythonish stuff and/or something else to generate them for me? Or, better yet, is there some syntactic trick I could use in the protocol definition to avoid needing them in the first place?

Thanks,
- Dave, who does not relish the thought of having to manually write equality tests.

Terms of Service

Privacy Policy

Cookie Policy