Skip to content

Commit af9bd94

Browse files
Hashable Conformance for Dictionary.Keys, CollectionOfOne and EmptyCollection (#3106)
* `Hashable` Conformance for `Dictionary.Keys`, `CollectionOfOne` and `EmptyCollection` * Update and rename NNNN-hashable-conformance-for-dictionarykeys-collectionofone-emptycollection.md to 0514-hashable-conformance-for-dictionarykeys-collectionofone-emptycollection.md * Update 0514-hashable-conformance-for-dictionarykeys-collectionofone-emptycollection.md * Update 0514-hashable-conformance-for-dictionarykeys-collectionofone-emptycollection.md --------- Co-authored-by: Stephen Canon <stephentyrone@gmail.com>
1 parent 94389ba commit af9bd94

1 file changed

Lines changed: 155 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# `Hashable` Conformance for `Dictionary.Keys`, `CollectionOfOne` and `EmptyCollection`
2+
3+
* Proposal: [SE-0514](0514-hashable-conformance-for-dictionarykeys-collectionofone-emptycollection.md)
4+
* Authors: [Clinton Nkwocha](https://github.com/clintonpi)
5+
* Review Manager: [Steve Canon](https://github.com/stephentyrone)
6+
* Status: **Active Review (February 20...March 6, 2026)**
7+
* Implementation: [swiftlang/swift#86899](https://github.com/swiftlang/swift/pull/86899)
8+
* Review: [pitch](https://forums.swift.org/t/pitch-hashable-conformance-for-dictionary-keys-collectionofone-and-emptycollection/84117)
9+
10+
## Introduction
11+
12+
This proposal adds `Hashable` conformance to three standard library collection types: `Dictionary.Keys`, `CollectionOfOne` and `EmptyCollection`.
13+
14+
## Motivation
15+
16+
### `Dictionary.Keys`
17+
18+
`Dictionary.Keys` is simply a view of the dictionary's keys, and every key in a dictionary conforms to `Hashable`. Hence, `Dictionary.Keys` should automatically and unconditionally conform to `Hashable`.
19+
20+
### `CollectionOfOne` and `EmptyCollection`
21+
`CollectionOfOne` and `EmptyCollection` are some of the more rarely used types and the addition of their `Hashable` conformance is more for completeness and consistency with other standard library collection types.
22+
23+
`CollectionOfOne` does not conform to `Equatable` so this proposal also adds that conformance.
24+
25+
## Proposed solution
26+
27+
The standard library should add unconditional `Hashable` conformance to `Dictionary.Keys` and `EmptyCollection`, and conditional `Equatable` and `Hashable` conformances to `CollectionOfOne`.
28+
29+
## Detailed design
30+
31+
### `Dictionary.Keys`
32+
33+
`Dictionary.Keys` gains unconditional `Hashable` conformance. Since dictionary keys are always `Hashable` (as required by `Dictionary`'s type constraints), the keys view is always hashable. The hash implementation uses a commutative hashing algorithm (XOR of individual element hashes) to ensure that two `Dictionary.Keys` collections hash to the same value if they contain the same elements regardless of iteration order.
34+
35+
```swift
36+
extension Dictionary.Keys {
37+
@_alwaysEmitIntoClient
38+
public func hash(into hasher: inout Hasher) {
39+
var commutativeHash = 0
40+
for element in self {
41+
// Note that, similar to `Set`'s and `Dictionary`'s hashing algorithms, we use a copy of our own hasher here.
42+
// This makes hash values dependent on its state, eliminating static collision patterns.
43+
var elementHasher = hasher
44+
elementHasher.combine(element)
45+
commutativeHash ^= elementHasher._finalize()
46+
}
47+
hasher.combine(commutativeHash)
48+
}
49+
50+
@_alwaysEmitIntoClient
51+
public var hashValue: Int { // Prevent compiler from synthesizing hashValue.
52+
var hasher = Hasher()
53+
self.hash(into: &hasher)
54+
return hasher.finalize()
55+
}
56+
}
57+
58+
@available(SwiftStdlib 6.3, *)
59+
extension Dictionary.Keys: Hashable {}
60+
```
61+
62+
For example:
63+
64+
```swift
65+
let batch1 = ["apple": 1, "banana": 2, "cherry": 3, "date": 4]
66+
let batch2 = ["date": 10, "banana": 20, "apple": 30, "cherry": 40]
67+
let batch3 = ["mango": 5, "orange": 6, "papaya": 7]
68+
69+
let uniqueBatches = Set([batch1.keys, batch2.keys, batch3.keys])
70+
71+
print(uniqueBatches)
72+
// [Dictionary.Keys(["orange", "mango", "papaya"]), Dictionary.Keys(["banana", "apple", "date", "cherry"])]
73+
```
74+
75+
### `CollectionOfOne`
76+
77+
`CollectionOfOne` gains conditional `Equatable` conformance when `Element` conforms to `Equatable`, and `Hashable` when `Element` conforms to `Hashable`. The hash value is derived from the single element it contains.
78+
79+
```swift
80+
extension CollectionOfOne where Element: Equatable {
81+
@_alwaysEmitIntoClient
82+
public static func ==(lhs: CollectionOfOne<Element>, rhs: CollectionOfOne<Element>) -> Bool {
83+
return lhs._element == rhs._element
84+
}
85+
}
86+
87+
extension CollectionOfOne where Element: Hashable {
88+
@_alwaysEmitIntoClient
89+
public func hash(into hasher: inout Hasher) {
90+
hasher.combine(self._element)
91+
}
92+
93+
@_alwaysEmitIntoClient
94+
public var hashValue: Int { // Prevent compiler from synthesizing hashValue.
95+
var hasher = Hasher()
96+
self.hash(into: &hasher)
97+
return hasher.finalize()
98+
}
99+
}
100+
101+
@available(SwiftStdlib 6.3, *)
102+
extension CollectionOfOne: Equatable where Element: Equatable {}
103+
104+
@available(SwiftStdlib 6.3, *)
105+
extension CollectionOfOne: Hashable where Element: Hashable {}
106+
```
107+
108+
### `EmptyCollection`
109+
110+
`EmptyCollection` gains unconditional `Hashable` conformance. Since all empty collections are equal regardless of their element type, they all hash to the same value. The hash function simply combines the value 0, consistent with the hashing convension for empty sets, dictionaries and arrays.
111+
112+
```swift
113+
extension EmptyCollection {
114+
@_alwaysEmitIntoClient
115+
public func hash(into hasher: inout Hasher) {
116+
hasher.combine(0)
117+
}
118+
119+
@_alwaysEmitIntoClient
120+
public var hashValue: Int { // Prevent compiler from synthesizing hashValue.
121+
var hasher = Hasher()
122+
self.hash(into: &hasher)
123+
return hasher.finalize()
124+
}
125+
}
126+
127+
@available(SwiftStdlib 6.3, *)
128+
extension EmptyCollection: Hashable {}
129+
```
130+
131+
## Source compatibility
132+
133+
This is a purely additive change, but any code that provides its own redundant conformance will now generate a warning (see "Implications on Adoption" for discussion of how to handle this).
134+
135+
## ABI compatibility
136+
137+
This proposal is purely an extension of the ABI of the standard library and does not change any existing features.
138+
139+
## Implications on adoption
140+
141+
The new conformances require Swift 6.4 or later. Adopters may simply declare the conformances when deploying to earlier Swift versions. For example:
142+
143+
```swift
144+
#if swift(<6.4)
145+
extension Dictionary.Keys: @retroactive Hashable {}
146+
#endif
147+
```
148+
149+
Note: if existing code on an earlier Swift version also implements these functions, there is a low, theoretical risk of binary compatibility issues at runtime if those implementations are fundamentally incompatible or conflict with the standard library implementations.
150+
151+
## Alternatives considered
152+
153+
### Don't include the `Hashable` conformance for `EmptyCollection`
154+
155+
Asides the reasons of completeness and consistency with other standard library collection types, use cases for working with `EmptyCollection`s in a hash-based context can be avoided (e.g. by working with `result ?? EmptyCollection<T>` instead). However, such workarounds may not be idiomatic for that use case.

0 commit comments

Comments
 (0)