Search code examples
swiftdictionaryswift-protocolsanyhashable

Swift compiler (dictionary key): "must conform to Hashable" and "cannot conform to Hashable". Huh?


I feel like the Swift compiler is giving me mixed messages.
I don't understand what the issue is.

This is what I have, and I want those enum to be keys to a dictionary:

protocol ArbitraryKey : CaseIterable, Hashable {}
enum HearNoEvil  : ArbitraryKey { case a, b, c }
enum SeeNoEvil   : ArbitraryKey { case d, e, f }
enum SpeakNoEvil : ArbitraryKey { case g, h, i }

This is what I want to avoid:

var dictionary : [ AnyHashable : String ] = [:]

This is what I want to do:

var dictionary : [ any ArbitraryKey : String ] = [:]

The protocol ArbitraryKey will eventually have methods I want to be able to invoke on the enums values.

However, the iOS 16.4 / Swift 5.7+ compiler objects to everything I've tried:


If  I  do  this:

protocol ArbitraryKey : CaseIterable, Hashable {}
var dictonary : [ArbitraryKey : String] = [:]
    Type 'any ArbitraryKey' cannot conform to 'Hashable'
    Use of protocol 'ArbitraryKey' as a type must be written 'any ArbitraryKey'
    Replace 'ArbitraryKey' with 'any ArbitraryKey'

So  I  "fix"  it:

protocol ArbitraryKey : CaseIterable, Hashable {}
var dictonary : [any ArbitraryKey : String] = [:]  // <-- added 'any'
     Type 'any ArbitraryKey' cannot conform to 'Hashable' 

So  I  "fix"  that:

protocol ArbitraryKey : CaseIterable {}  // <-- removed Hashable
var dictonary : [any ArbitraryKey : String] = [:]
     Type 'any ArbitraryKey' does not conform to protocol 'Hashable'

In conclusion:

At this point the chump is stumped.
Can someone explain what is going on?
Any thoughts on a different approach that's clean and straightforward?


Solution

  • The basic question you're asking in your title is how the compile errors "must conform to Hashable" and "cannot conform to Hashable" can be compatible. They are not just compatible; they are the same.

    As I've explained to you in a comment, when you declare a dictionary as being of type [Foo: Bar], you are resolving the Key and Value generic placeholders. Only the name of a concrete type can do that. And for Key the type name that you put must additionally be the name of a concrete type that adopts Hashable.

    Well, a protocol is not that. It isn't a concrete type, and it cannot conform to Hashable (protocols cannot conform to protocols). Indeed, that is exactly why Swift 5.8 forces you to say any, so that you are brought face to face with the facts. When the compiler makes you say any MyProtocol it is trying to make you understand that this is an existential, not a concrete type.

    Anyway so the answer to the question posed in your title is that the compiler is saying very clearly, "What goes here must be a type that conforms to Hashable, and a protocol existential cannot conform to Hashable so it cannot go here."

    With regard to your comment:

    I want to index that dictionary with any of a few kinds of enums, which are concrete

    Okay, well, that seems to me to be a silly thing to want to do, but the point is, if you were to do that, you would not be able to do it by making any sort of type declaration of the Key type of your dictionary, for the reasons I've just demonstrated. You would need to perform some sort of type erasure.

    For example, you could hide from the dictionary the fact that all its keys enums that conform to a certain protocol, by making that fact something that you know rather than trying to make the dictionary know it. To do so, you might just wrap the dictionary in a class that guards the doors so that the only acceptable keys are types that conform to your protocol. Like this:

    protocol ArbitraryKey : CaseIterable, Hashable {}
    enum HearNoEvil  : ArbitraryKey { case a, b, c }
    enum SeeNoEvil   : ArbitraryKey { case d, e, f }
    enum SpeakNoEvil : ArbitraryKey { case g, h, i }
    
    class DictionaryWrapper {
        var dict = [AnyHashable: String]()
        func setKey<T: ArbitraryKey>(_ key: T, toValue value: String) {
            dict[key] = value
        }
        func getValueForKey<T: ArbitraryKey>(_ key: T) -> String? {
            return dict[key]
        }
    }
    

    That may seem rather silly, and indeed it is; and it's only an example. But it does succeed very simply in doing what you have (for reasons known only to you) asked to do: We have guaranteed to the caller of our code (such as setKey and getValueForKey that this key is a type that conforms to the ArbitraryKey protocol. So you could just go on writing methods for our DictionaryWrapper until you've accomplished whatever your ultimate goal is.


    [Extra Point:] I think part of the confusion here is in the grasp of the meaning of expressions such as protocol ArbitraryKey : Hashable {}. You asked about this in a comment, in a way that made me think you think this means that ArbitraryKey conforms to Hashable. It doesn't. Protocols don't conform to protocols. That code is talking about the concrete type that will conform to this protocol (ArbitraryKey). It says: "To conform to me, you must also (in addition to any requirements that I may impose) conform to Hashable." It isn't a statement about what the protocol does, it's a statement about what the conforming type has to do in order to conform.