Skip to content

Commit 802c90d

Browse files
allevatolattner
authored andcommitted
Proposal to synthesize Equatable/Hashable for value types. (#706)
* Proposal to synthesize Equatable/Hashable for value types. * Updates based on the WIP implementation * Update to match the proposal template * Remove the _original type declaration_ requirement * Fix typos * Forbid synthesis in extensions (see SR-4920).
1 parent e029d9d commit 802c90d

File tree

1 file changed

+345
-0
lines changed

1 file changed

+345
-0
lines changed
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
# Synthesizing `Equatable` and `Hashable` conformance
2+
3+
* Proposal: [SE-NNNN](NNNN-synthesize-equatable-hashable.md)
4+
* Author: [Tony Allevato](https://github.com/allevato)
5+
* Review Manager: TBD
6+
* Status: **Awaiting review**
7+
8+
## Introduction
9+
10+
Developers have to write large amounts of boilerplate code to support
11+
equatability and hashability of complex types. This proposal offers a way for
12+
the compiler to automatically synthesize conformance to `Equatable` and
13+
`Hashable` to reduce this boilerplate, in a subset of scenarios where generating
14+
the correct implementation is known to be possible.
15+
16+
Swift-evolution thread: [Universal Equatability, Hashability, and Comparability
17+
](https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160307/012099.html)
18+
19+
## Motivation
20+
21+
Building robust types in Swift can involve writing significant boilerplate
22+
code to support hashability and equatability. By eliminating the complexity for
23+
the users, we make `Equatable`/`Hashable` types much more appealing to users and
24+
allow them to use their own types in contexts that require equatability and
25+
hashability with no added effort on their part (beyond declaring the
26+
conformance).
27+
28+
Equality is pervasive across many types, and for each one users must implement
29+
the `==` operator such that it performs a fairly rote memberwise equality test.
30+
As an example, an equality test for a basic `struct` is fairly uninteresting:
31+
32+
```swift
33+
struct Person: Equatable {
34+
static func == (lhs: Person, rhs: Person) -> Bool {
35+
return lhs.firstName == rhs.firstName &&
36+
lhs.lastName == rhs.lastName &&
37+
lhs.birthDate == rhs.birthDate &&
38+
...
39+
}
40+
}
41+
```
42+
43+
What's worse is that this operator must be updated if any properties are added,
44+
removed, or changed, and since it must be manually written, it's possible to get
45+
it wrong, either by omission or typographical error.
46+
47+
Likewise, hashability is necessary when one wishes to store a type in a
48+
`Set` or use one as a multi-valued `Dictionary` key. Writing high-quality,
49+
well-distributed hash functions is not trivial so developers may not put a great
50+
deal of thought into them—especially as the number of properties
51+
increases—not realizing that their performance could potentially suffer
52+
as a result. And as with equality, writing it manually means there is the
53+
potential for it to not only be inefficient, but incorrect as well.
54+
55+
In particular, the code that must be written to implement equality for
56+
`enum`s is quite verbose:
57+
58+
```swift
59+
enum Token: Equatable {
60+
case string(String)
61+
case number(Int)
62+
case lparen
63+
case rparen
64+
65+
static func == (lhs: Token, rhs: Token) -> Bool {
66+
switch (lhs, rhs) {
67+
case (.string(let lhsString), .string(let rhsString)):
68+
return lhsString == rhsString
69+
case (.number(let lhsNumber), .number(let lhsNumber)):
70+
return lhsNumber == rhsNumber
71+
case (.lparen, .lparen), (.rparen, .rparen):
72+
return true
73+
default:
74+
return false
75+
}
76+
}
77+
}
78+
```
79+
80+
Crafting a high-quality hash function for this `enum` would be similarly
81+
inconvenient to write.
82+
83+
Swift already derives `Equatable` and `Hashable` conformance for a small subset
84+
of `enum`s: those for which the cases have no associated values (which includes
85+
enums with raw types). Two instances of such an `enum` are equal if they are the
86+
same case, and an instance's hash value is its ordinal:
87+
88+
```swift
89+
enum Foo {
90+
case zero, one, two
91+
}
92+
93+
let x = (Foo.one == Foo.two) // evaluates to false
94+
let y = Foo.one.hashValue // evaluates to 1
95+
```
96+
97+
Likewise, conformance to `RawRepresentable` is automatically derived for `enum`s
98+
with a raw type, and the recently approved `Encodable`/`Decodable` protocols
99+
also support synthesis of their operations when possible. Since there is
100+
precedent for synthesized conformances in Swift, we propose extending it to
101+
these fundamental protocols.
102+
103+
## Proposed solution
104+
105+
In general, we propose that a type synthesize conformance to
106+
`Equatable`/`Hashable` if all of its members are `Equatable`/`Hashable`. We
107+
describe the specific conditions under which these conformances are synthesized
108+
below, followed by the details of how the conformance requirements are
109+
implemented.
110+
111+
### Requesting synthesis is opt-in
112+
113+
Users must _opt-in_ to automatic synthesis by declaring their type as
114+
`Equatable` or `Hashable` without implementing any of their requirements. This
115+
conformance must be part of the _original type declaration_ and not on an
116+
extension (see [Synthesis in extensions](#synthesis-in-extensions) below for
117+
more on this).
118+
119+
Any type that declares such conformance and satisfies the conditions below
120+
will cause the compiler to synthesize an implementation of `==`/`hashValue`
121+
for that type.
122+
123+
Making the synthesis opt-in—as opposed to automatic derivation without
124+
an explicit declaration—provides a number of benefits:
125+
126+
* The syntax for opting in is natural; there is no clear analogue in Swift
127+
today for having a type opt out of a feature.
128+
129+
* It requires users to make a conscious decision about the public API surfaced
130+
by their types. Types cannot accidentally "fall into" conformances that the
131+
user does not wish them to; a type that does not initially support `Equatable`
132+
can be made to at a later date, but the reverse is a breaking change.
133+
134+
* The conformances supported by a type can be clearly seen by examining
135+
its source code; nothing is hidden from the user.
136+
137+
* We reduce the work done by the compiler and the amount of code generated
138+
by not synthesizing conformances that are not desired and not used.
139+
140+
* As will be discussed later, explicit conformance significantly simplifies
141+
the implementation for recursive types.
142+
143+
There is one exception to this rule: the current behavior will be preserved that
144+
`enum` types with cases that have no associated values (including those with raw
145+
values) conform to `Equatable`/`Hashable` _without_ the user explicitly
146+
declaring those conformances. While this does add some inconsistency to `enum`s
147+
under this proposal, changing this existing behavior would be source-breaking.
148+
The question of whether such `enum`s should be required to opt-in as well can
149+
be revisited at a later date if so desired.
150+
151+
### Overriding synthesized conformances
152+
153+
Any user-provided implementations of `==` or `hashValue` will override the
154+
default implementations that would be provided by the compiler.
155+
156+
### Conditions where synthesis is allowed
157+
158+
For brevity, let `P` represent either the protocol `Equatable` or `Hashable` in
159+
the descriptions below.
160+
161+
#### Synthesized requirements for `enum`s
162+
163+
For an `enum`, synthesis of `P`'s requirements is based on the conformances of
164+
its cases' associated values. Computed properties are not considered.
165+
166+
The following rules determine whether `P`'s requirements can be synthesized for
167+
an `enum`:
168+
169+
* The compiler does **not** synthesize `P`'s requirements for an `enum` with no
170+
cases because it is not possible to create instances of such types.
171+
172+
* The compiler synthesizes `P`'s requirements for an `enum` with one or more
173+
cases if and only if all of the associated values of all of its cases conform
174+
to `P`.
175+
176+
#### Synthesized requirements for `struct`s
177+
178+
For a `struct`, synthesis of `P`'s requirements is based on the conformances of
179+
**only** its stored instance properties. Neither static properties nor computed
180+
instance properties (those with custom getters) are considered.
181+
182+
The following rules determine whether `P`'s requirements can be synthesized for
183+
a `struct`:
184+
185+
* The compiler trivially synthesizes `P`'s requirements for a `struct` with *no*
186+
stored properties. (All instances of a `struct` with no stored properties can
187+
be considered equal and hash to the same value if the user opts in to this.)
188+
189+
* The compiler synthesizes `P`'s requirements for a `struct` with one or more
190+
stored properties if and only if all of the types of all of its stored
191+
properties conform to `P`.
192+
193+
### Considerations for recursive types
194+
195+
By making the synthesized conformances opt-in, recursive types have their
196+
requirements fall into place with no extra effort. In any cycle belonging to a
197+
recursive type, every type in that cycle must declare its conformance
198+
explicitly. If a type does so but cannot have its conformance synthesized
199+
because it does not satisfy the conditions above, then it is simply an error for
200+
_that_ type and not something that must be detected earlier by the compiler in
201+
order to reason about _all_ the other types involved in the cycle. (On the other
202+
hand, if conformance were implicit, the compiler would have to fully traverse
203+
the entire cycle to determine eligibility, which would make implementation much
204+
more complex).
205+
206+
### Implementation details
207+
208+
An `enum T: Equatable` that satisfies the conditions above will receive a
209+
synthesized implementation of `static func == (lhs: T, rhs: T) -> Bool` that
210+
returns `true` if and only if `lhs` and `rhs` are the same case and have
211+
payloads that are memberwise-equal.
212+
213+
An `enum T: Hashable` that satisfies the conditions above will receive a
214+
synthesized implementation of `var hashValue: Int { get }` that uses an
215+
unspecified hash function<sup>†</sup> to compute the hash value by incorporating
216+
the case's ordinal (i.e., definition order) followed by the hash values of its
217+
associated values as its terms, also in definition order.
218+
219+
A `struct T: Equatable` that satisfies the conditions above will receive a
220+
synthesized implementation of `static func == (lhs: T, rhs: T) -> Bool` that
221+
returns `true` if and only if `lhs.x == rhs.x` for all stored properties `x` in
222+
`T`. If the `struct` has no stored properties, this operator simply returns
223+
`true`.
224+
225+
A `struct T: Hashable` that satisfies the conditions above will receive a
226+
synthesized implementation of `var hashValue: Int { get }` that uses an
227+
unspecified hash function<sup>†</sup> to compute the hash value by incorporating
228+
the hash values of the fields as its terms, in definition order. If the `struct`
229+
has no stored properties, this property evaluates to a fixed value not specified
230+
here.
231+
232+
<sup>†</sup> The choice of hash function is left as an implementation detail,
233+
not a fixed part of the design; as such, users should not depend on specific
234+
characteristics of its behavior. The most likely implementation would call the
235+
standard library's `_mixInt` function on each member's hash value and then
236+
combine them with exclusive-or (`^`), which mirrors the way `Collection` types
237+
are hashed today.
238+
239+
## Source compatibility
240+
241+
By making the conformance opt-in, this is a purely additive change that does
242+
not affect existing code. We also avoid source-breaking changes by not changing
243+
the behavior for `enum`s with no associated values, which will continue to
244+
implicitly conform to `Equatable` and `Hashable` even without explicitly
245+
declaring the conformance.
246+
247+
## Effect on ABI stability
248+
249+
This feature is purely additive and does not change ABI.
250+
251+
## Effect on API resilience
252+
253+
N/A.
254+
255+
## Alternatives considered
256+
257+
In order to realistically scope this proposal, we considered but ultimately
258+
deferred the following items, some of which could be proposed additively in the
259+
future.
260+
261+
### Synthesis in extensions
262+
263+
Requirements will be synthesized only for protocol conformances that are
264+
_part of the type declaration itself;_ conformances added in extensions will
265+
not be synthesized.
266+
267+
For `struct`s, synthesizing a requirement would not be safe in an extension
268+
in a different module or in a different file in the same module because any
269+
`private` or `fileprivate` members of the `struct` would not be accessible
270+
there. Extensions within the same file would be safe now that `private` members
271+
are also accessible from extensions of the containing type in the same file.
272+
273+
However, to align with `Codable` in the context of
274+
[SR-4920](https://bugs.swift.org/browse/SR-4920), we will also currently
275+
forbid synthesized requirements in extensions in the same file; this specific
276+
case can be revisited later for all derived conformances.
277+
278+
We note that conformances to `enum` types would be safe to synthesize anywhere
279+
because the cases and their associated values are always as accessible as the
280+
`enum` type itself, but we apply the same rule above for consistency; users do
281+
not have to memorize an intricate table of what is derivable and where.
282+
283+
### Synthesis for `class` types and tuples
284+
285+
We do not synthesize conformances for `class` types. The conditions above become
286+
more complicated in inheritance hierarchies, and equality requires that
287+
`static func ==` be implemented in terms of an overridable instance method for
288+
it to be dispatched dynamically. Even for `final` classes, the conditions are
289+
not as clear-cut as they are for value types because we have to take superclass
290+
behavior into consideration. Finally, since objects have reference identity,
291+
memberwise equality may not necessarily imply that two instances are equal.
292+
293+
We do not synthesize conformances for tuples at this time. While this would
294+
nicely round out the capabilities of value types, allow the standard library to
295+
remove the hand-crafted implementations of `==` for up-to-arity-6 tuples, and
296+
allow those types to be used in generic contexts where `Equatable` conformance
297+
is required, adding conformances to non-nominal types would require additional
298+
work.
299+
300+
### Omitting fields from synthesized conformances
301+
302+
Some commenters have expressed a desire to tag certain properties of a `struct`
303+
from being included in automatically generated equality tests or hash value
304+
computations. This could be valuable, for example, if a property is merely used
305+
as an internal cache and does not actually contribute to the "value" of the
306+
instance. Under the rules above, if this cached value was equatable, a user
307+
would have to override `==` and `hashValue` and provide their own
308+
implementations to ignore it.
309+
310+
Such a feature, which could be implemented with an attribute such as
311+
`@transient`, would likely also play a role in other protocols like
312+
`Encodable`/`Decodable`. This could be done as a purely additive change on top
313+
of this proposal, so we propose not doing this at this time.
314+
315+
### Implicit derivation
316+
317+
An earlier draft of this proposal made derived conformances implicit (without
318+
declaring `Equatable`/`Hashable` explicitly). This has been changed
319+
because&mdash;in addition to the reasons mentioned earlier in the
320+
proposal&mdash;`Encodable`/`Decodable` provide a precedent for having the
321+
conformance be explicit. More importantly, however, determining derivability for
322+
recursive types is _significantly more difficult_ if conformance is implicit,
323+
because it requires examining the entire dependency graph for a particular type
324+
and to properly handle cycles in order to decide if the conditions are
325+
satisfied.
326+
327+
### Support for `Comparable`
328+
329+
The original discussion thread also included `Comparable` as a candidate for
330+
automatic generation. Unlike equatability and hashability, however,
331+
comparability requires an ordering among the members being compared.
332+
Automatically using the definition order here might be too surprising for users,
333+
but worse, it also means that reordering properties in the source code changes
334+
the code's behavior at runtime. (This is true for hashability as well if a
335+
multiplicative hash function is used, but hash values are not intended to be
336+
persistent and reordering the terms does not produce a significant _behavioral_
337+
change.)
338+
339+
## Acknowledgments
340+
341+
Thanks to Joe Groff for spinning off the original discussion thread, Jose Cheyo
342+
Jimenez for providing great real-world examples of boilerplate needed to support
343+
equatability for some value types, Mark Sands for necromancing the
344+
swift-evolution thread that convinced me to write this up, and everyone on
345+
swift-evolution since then for giving me feedback on earlier drafts.

0 commit comments

Comments
 (0)