[Pitch] Allow coding of non-`String`/`Int` keyed `Dictionary` into a `KeyedContainer`

This is some excellent feedback! One thing I wanted to respond to:

When @Morten_Bek_Ditlevsen and I discussed some of the topics here originally, this subject came up and I suggested that CodingKeyRepresentable not have an associatedtype — I wanted to share some of those reasons for posterity, and in case they might influence the decision of where to go here.

Because associatedtypes have non-zero cost on the consuming side (e.g. checking for CodingKeyRepresentable conformance, using the key type), I think that the associated type definition would need to carry its weight. Despite the name, I think that the key difference between CodingKeyRepresentable and RawRepresentable is that the identity of the RawValue type is crucial to RawRepresentable, but not so in the CodingKeyRepresentable case.

On the consuming side of CodingKeyRepresentable.codingKey (e.g. in Dictionary), I don't believe key type identity is necessarily useful enough:

  • The main use for the .codingKey value is immediate retrieval of the underlying String/Int values. Dictionary would either pull those values out for immediate use and throw away the original key
  • In a non-generic context (or even one not predicated on CodingKeyRepresentable conformance), you can't meaningfully get at the key type. The type-erasure song and dance you have to do to get the key values won't be able to hand you a typed key (and the pain of doing that dance is that because it doesn't make sense to expose a public protocol for doing the erasure, every consumer that wants to do this needs to reinvent the wheel and add another protocol for doing it; we had to do it a few times for Optionals and it's a bit of a shame)
  • Even if it were necessary to get a meaningful key type, the majority use-case for this feature, I believe, will be to provide dynamic-value keys for non-enumerable types (e.g. structs like UUID [though yes, we can't make it conform]); for these types, you can't necessarily define a CodingKeys enum and instead, you'd likely want to use a more generic key type like AnyCodingKey (which by definition doesn't have identity)

On the producing side (e.g. in MyCustomType), I'm also not sure the utility is necessarily enough: in general, the majority of CodingKeyRepresentable types (I believe) will only really care about the String/Int values of the keys, since they will be initialized dynamically (again, I think of UUID initialization from a CodingKey.stringValue — you can do this from any CodingKey).

I believe that the constrained MyKey example above will be the minority use-case, but expressed without the associatedtype constraint too:

enum MyKey: Int, CodingKey {
    case a = 1, b = 3, c = 5

    // There are several ways to express this, just an example:
    init?(codingKey: CodingKey) {
        if let key = codingKey.intValue.flatMap(Self.init(intValue:)) {
            self = key
        } else if let key = Self(stringValue: codingKey.stringValue) {
            self = key
        } else {
            return nil
        }
    }
}

struct MyCustomType: CodingKeyRepresentable {
    var useB = false

    var codingKey: CodingKey {
        useB ? MyKey.b : MyKey.a
    }

    init?(codingKey: CodingKey) {
        switch MyKey(codingKey: codingKey) {
        case .a: useB = false
        case .b: useB = true
        default: return nil
        }    
    }
}

I personally find this equally as expressive, and I think that not requiring the associated type gives more flexibility without a significant loss, especially with non-enum types in mind. What do you think? (If MyCustomType here was inspired by a real-world example, I'd love to know more about it!)

1 Like