Gregory Higley
Gregory Higley

Reputation: 16558

Hiding Hashable in Swift

How can I return [Hashable: Any] where the concrete type implementing Hashable is hidden?

Swift has two types of protocols: Those that can be used as types and those that can only be used as type constraints. While this is powerful, it is also unfortunate. It means that certain types of information hiding are not directly possible.

The only solution I could come up with is to use a thunk:

struct Hash: Hashable {
    private let value: Any
    private let equals: Hash -> Bool

    init<H: Hashable>(_ h: H) {
        self.value = h
        self.hashValue = h.hashValue
        self.equals = { ($0.value as! H) == h }
    }

    let hashValue: Int
}

func ==(lhs: Hash, rhs: Hash) -> Bool {
    return lhs.equals(rhs)
}

Perhaps someday Swift will create thunks like this for us. That is after all one of the things compilers are for.

Is this the only way? Am I missing something?

Upvotes: 2

Views: 773

Answers (1)

Kevin
Kevin

Reputation: 17556

The reason behind this is that swift protocols like Hashable can be applied to all different types, not just classes.

Let's say we have two different types: a struct which takes up 8 bits of memory and a class which is a pointer to some chunk of memory on the heap.

struct HashyStruct : Equatable, Hashable {
    let smallNumber: UInt16
    var hashValue: Int {
        return Int(smallNumber)
    }
}
func ==(lhs: HashyStruct, rhs: HashyStruct) -> Bool {
    return lhs.smallNumber == rhs.smallNumber
}


class HashyClass : Equatable, Hashable {
    let number: UInt64
    init(number: UInt64) {
        self.number = number
    }
    var hashValue: Int {
        return Int(number)
    }
}
func ==(lhs: HashyClass, rhs: HashyClass) -> Bool {
    return lhs.number == rhs.number
}

Both of these types are Hashable, so why can't we have an dictionary like this:

let anyHashable: [Hashable:Any] = [HashyStruct(smallNumber: 5) : "struct", HashyClass(number: 0x12345678):"class"]

error: using 'Hashable' as a concrete type conforming to protocol 'Hashable' is not supported error: protocol 'Hashable' can only be used as a generic constraint because it has Self or associated type requirements

Dictionary needs to be able to compare objects to each other to resolve conflicts where two things have the same hash value. How would it compare a HashyStruct to a HashyClass? It has no idea which function to call to compare the two objects. It doesn't know whether to call hashValue on HashyStruct or HashyClass.

But Objective-C does it...

You can actually implement this if you want to do it dynamically at runtime. The "thunk" you implemented does have that sort of dynamic behavior but it comes with a performance hit that in most cases you don't need to take.

More juicy hash table details which aren't super important but help you understand

I'm assuming you know how hash tables are implemented as a fixed size array and the hash value is mapped to a limited number of buckets which can have conflicts.

Dictionary allocates a chunk of memory to store it's objects; lets give it 4 portions of 64 bytes each (the size of a pointer on 64 bit machines).

| 64 bytes | 64 bytes | 64 bytes | 64 bytes |

When you add two items with hash values 1 and 6 = 2 % 4 you'll get the following hash table:

| empty | item 1 | item 2 | empty |

That's all well and good; we can fit any number of pointer objects into our table which will work for classes. But in swift we have structs - HashyStruct only has 16 bits. If we gave the same amount of memory to a dictionary storing HashyStructs then we could store 16 instead of 4 items in our hash set.

| 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes | 16 bytes |

As long as the compiler knows the size of the type we can have any type we want in a hash table. But when we have two different types...???

| 4 structs | item 1 | item 2 | empty |

You get a hash table which makes no sense. The compiler doesn't know the size of the items in the array so it can't index them.

A lot of what makes swift great is that you aren't tied to classes and objects allocated on the heap. It by default gives you good performance and you opt into dynamic behavior if you need it.

Upvotes: 1

Related Questions