@dynamicMemberLookup introduces ambiguity

As of Xcode11 GM the following problem is present:

@dynamicMemberLookup public struct Tagged<Tag, RawValue> {
  public var rawValue: RawValue
  public init(rawValue: RawValue) {
    self.rawValue = rawValue
  }
  public subscript<Subject>(dynamicMember keyPath: KeyPath<RawValue, Subject>) -> Subject {
    rawValue[keyPath: keyPath]
  }
}

extension Tagged: Hashable, Equatable where RawValue: Hashable {
  public func hash(into hasher: inout Hasher) {
    hasher.combine(self.rawValue)
  }
}

func testHashable() {
  enum Tag { }
  Tagged<Tag, Int>(rawValue: 1).hashValue //ambiguous reference to member hashValue
}

I.e. theres ambiguity between the default implementation of hashValue inherited from Hashable and accessing the underlying rawValue.hashValue via the [dynamicMember:] subscript. The latter is still accessible via the subscript (i.e. Tagged<Tag,Int>(rawValue: 1)[dynamicMember: \.hashValue]) but former is almost completely lost. The types exactly match up, and acessing via [keyPath:] subscript is impossible since creating the KeyPath runs into the same problem. as Hashable is not an option because of associated types.

I was able to recover the Hashable default implementation using opaque result types like this:

extension Tagged where RawValue: Hashable {
  public var asHashable: some Hashable {
    self
  }
}
//...
Tagged<Tag, Int>(rawValue: 1).asHashable.hashValue //compiles

However, afaik this solution cannot be generalized and the callsite is still ugly anyway.

The Tagged example is the most obvious but not the only valid usecase, Firebase + Combine extensions · GitHub may be another one.
The Hashable example is not the only usecase, same is true of any other conditional protocol conformance with default implementations.

Any ideas how to workaround this limitation?
I guess Im perfectly happy if it doesnt work with the straightforward syntax but feel like we should at least be able to get at it via KeyPaths. To me, the decision to not require \.[dynamicMember:...] when creating dynamicMemberLookup KeyPaths is questionnable.
Alternatively, I would be happy if the "priority" of resolving dynamicMemberLookup got lowered all the way down so that anything else (even inherited from extensions) shadowed it. I feel like this is a good default for most cases and [dynamicMember:] is still always accessible if needed.

Thanks for any answers.

Alternatively, I would be happy if the "priority" of resolving dynamicMemberLookup got lowered all the way down so that anything else (even inherited from extensions) shadowed it.

I think that's what the solution is here, we'd want to add dynamic member lookup in cases when all other possible candidates come from conditional conformances, because there is no guarantee they are going to match.

2 Likes

@pteasima I have opened SR - [SR-11465] Ambiguity in expression which matches both dynamic member lookup and declaration from constrained extension · Issue #53865 · apple/swift · GitHub and have a PR ready to fix it - [CSRanking] Always rank key path dynamic member choices lower than no… by xedin · Pull Request #27146 · apple/swift · GitHub

3 Likes

Changes have been merged to master/5.1. @pteasima you can use the next available nightly to validate.

4 Likes

Confirmed working for my usecase on September13 nightly. Thanks for the awesome and super quick fix Pavel!

Fine to mark this thread as resolved but I dont see a way to do that, anyone with the power feel free..

Wow, thanks @xedin !