|
| 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—in addition to the reasons mentioned earlier in the |
| 320 | +proposal—`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