Make types Hashable

Currently Swift types arent Hashable. This is a problem e.g. in SwiftUI Environment, where theres a subscript<K: EnvironmentKey>(type: K.Type) but it cant be used directly @Environment(\.[MyKey.self]). The result is needless boilerplate (proxy property) in one of SwiftUI's core features.

I worked around this in GitHub - pteasima/BetterEnvironment: SwiftUI Sugar. , but still its not as elegant as making the subscript available in a key path.

With how easy the solution seems to be (struct HashableType<Type>: Hashable { }), arguably it would be easy to add to Swift (the only thing stopping users from doing it is the fact that you cant extend metatypes). Im not a compiler expert and am ready to be educated. But stil, is this worth pursuing?

10 Likes

Formally, they're called Metatype.

2 Likes

You could probably do something like this:

struct AnyMetatypeWrapper {
    let metatype: Any.Type
}

extension AnyMetatypeWrapper: Equatable {
    static func ==(lhs: Self, rhs: Self) -> Bool {
        lhs.metatype == rhs.metatype
    }
}

extension AnyMetatypeWrapper: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(ObjectIdentifier(metatype))
    }
}

extension Dictionary {
    subscript(_ key: Any.Type) -> Value? where Key == AnyMetatypeWrapper {
        get { self[AnyMetatypeWrapper(metatype: key)] }
        _modify { yield &self[AnyMetatypeWrapper(metatype: key)] }
    }
}

var dict: [AnyMetatypeWrapper: Any] = [:]
dict[String.self] = "String"
dict[Int.self] = "Int"
print(dict[String.self]!) // String
print(dict[Int.self]!) // Int

This is an example of wrapping metatypes and using ObjectIdentifier for hashing. This is quite useful when you want to use metatypes as keys for dictionary (which requires Hashable). Perhaps you can adapt this to what you want to do?

2 Likes

Nice, I think this is another solution (this time AnyMetatypeWrapper isnt generic over the type, unlike my solution).

But the problem is that your subscript still cant be used in a KeyPath, which is what I was going for all along and perhaps didnt make it clear enough with the @Environment example.

I am aware that you can make a version of the subscript that takes something hashable, instead of Any.Type. But thats what Im trying to avoid (and why Im on Swift evolution forums, since I believe it needs to be a Swift feature).

I would really love to see this as a language feature as well. Is this something that seems viable to implement, or are there complications that would make this difficult?

I would love one day to make a huge breaking change and implement Universal Equality and Hashability:

  1. Provide global func ==<T>(lhs: T, rhs: T) handling any type, including meta types, tuples, existential containers and functions. Similar for hashing.
  2. Move equality and hashability witness to VWT.
  3. Replace Equatable/Hashable with CustomEquatable/CustomHashable similar to CustomStringConvertible and CustomReflectable.
  4. Drop === and use == to compare references. Disallow classes to conform to CustomEquatable/CustomHashable.

Reasoning for the last one - if reference equality is not what you need, then your entity should not be modeled as a reference type, but rather a value type that uses heap allocation as its implementation detail.

EDIT:

  1. And it also requires stable weak reference - currently references to zombie side tables are dropped during copying.
3 Likes