[swift-evolution] Pitch: Automatically deriving Equatable/Hashable for more value types

Andrew Bennett cacoyi at gmail.com
Thu May 4 17:10:58 CDT 2017


I agree, let's make it opt-in.

This looks really great, I'm excited to get generated conformance for
Equatable/Hashable in time for Swift 4.

I think it's worth mentioning that Encoding/Decoding generated conformance
is already accepted and implemented in Swift 4. The implementation and
acceptance criterion for Equatable/Hashable is likely to be very similar.

*For the open questions*, I think for the sake of getting this into Swift 4
we should go for explicit derivation, and don't allow omission of fields
(yet).

Omission is nice-to-have, but likely to be a long-winded bike-shed.

Changing from explicit to implicit is a loss of information, and has a
large impact on the language, it can't easily be undone, so it requires a
large discussion when it's decided. It only adds a little additional
convenience to the user though.

I suggest we discuss implicit generation and allowing omission with
follow-up proposals, they will very likely be additive non-breaking
changes. For this proposal we play it safe and stick to explicit
conformance and no omission of fields.


On Fri, May 5, 2017 at 8:01 AM, Xiaodi Wu via swift-evolution <
swift-evolution at swift.org> wrote:

> Hmm, I can see the appeal of automatically deriving Equatable and Hashable
> conformance, but I'd like that to be opt-in. That is, types should declare
> that they are Equatable or Hashable to begin with. It wouldn't have to take
> extra syntax, as compiler magic could effectively synthesize default
> implementations for == and/or hashValue when all members are themselves
> Equatable or Hashable, respectively. With such a scheme, consideration can
> be made to accommodating classes too.
> On Thu, May 4, 2017 at 15:37 Tony Allevato via swift-evolution <
> swift-evolution at swift.org> wrote:
>
>> Hi all,
>>
>> A conversation on Twitter last night brought up some interest in this
>> feature and I was encouraged to revive this proposal.
>>
>> Jordan Rose mentioned
>> <https://twitter.com/UINT_MIN/status/859922619578986496> on Twitter that
>> it could possibly make it in by the Swift 4 deadline if others
>> contributed—I have a WIP branch (albeit not currently working because I
>> rebased after a couple months of it being idle) that does the work for
>> enums but I got stuck on the mutually recursive cases. If this got
>> approved, I'd love to collaborate with other interested folks to finish up
>> the implementation.
>>
>> Link: https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad
>>
>>
>> Deriving Equatable and Hashable for value types
>>
>>    - Proposal: SE-0000
>>    <https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-name.md>
>>    - Author(s): Tony Allevato <https://github.com/allevato>
>>    - Status: Awaiting review
>>    <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#rationale>
>>    - Review manager: TBD
>>
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#introduction>
>> Introduction
>>
>> Value types are prevalent throughout the Swift language, and we encourage
>> developers to think in those terms when writing their own types.
>> Frequently, developers have to write large amounts of boilerplate code to
>> support equatability and hashability of value types. This proposal offers a
>> way for the compiler to automatically derive conformance to Equatable and
>>  Hashable to reduce this boilerplate, in a subset of scenarios where
>> generating the correct implementation is known to be possible.
>>
>> Swift-evolution thread: Universal Equatability, Hashability, and
>> Comparability
>> <http://thread.gmane.org/gmane.comp.lang.swift.evolution/8919>
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#motivation>
>> Motivation
>>
>> Building robust value types in Swift can involve writing significant
>> boilerplate code to support hashability and equatability. Equality is
>> pervasive across many value types, and for each one users must implement the
>>  == operator such that it performs a fairly rote memberwise equality
>> test. As an example, an equality test for a struct looks fairly
>> uninteresting:
>>
>> struct Foo: Equatable {
>>   static func == (lhs: Foo, rhs: Foo) -> Bool {
>>     return lhs.property1 == rhs.property1 &&
>>            lhs.property2 == rhs.property2 &&
>>            lhs.property3 == rhs.property3 &&
>>            ...
>>   }
>> }
>>
>> What's worse is that this operator must be updated if any properties are
>> added, removed, or changed, and since it must be manually written, it's
>> possible to get it wrong, either by omission or typographical error.
>>
>> Likewise, hashability is necessary when one wishes to store a value type
>> in a Set or use one as a multi-valued Dictionary key. Writing
>> high-quality, well-distributed hash functions is not trivial so developers
>> may not put a great deal of thought into them – especially as the number of
>> properties increases – not realizing that their performance could
>> potentially suffer as a result. And as with equality, writing it manually
>> means there is the potential to get it wrong.
>>
>> In particular, the code that must be written to implement equality for
>> enums is quite verbose:
>>
>> enum Token: Equatable {
>>   case string(String)
>>   case number(Int)
>>   case lparen
>>   case rparen
>>
>>   static func == (lhs: Token, rhs: Token) -> Bool {
>>     switch (lhs, rhs) {
>>     case (.string(let lhsString), .string(let rhsString)):
>>       return lhsString == rhsString
>>     case (.number(let lhsNumber), .number(let lhsNumber)):
>>       return lhsNumber == rhsNumber
>>     case (.lparen, .lparen), (.rparen, .rparen):
>>       return true
>>     default:
>>       return false
>>     }
>>   }
>> }
>>
>> Crafting a high-quality hash function for this enum would be similarly
>> inconvenient to write.
>>
>> Swift already derives Equatable and Hashable conformance for a small
>> subset of enums: those for which the cases have no associated values
>> (including enums with raw types). Two instances of such an enum are equal
>> if they are the same case, and an instance's hash value is its ordinal:
>>
>> enum Foo  {
>>   case zero, one, two
>> }
>> let x = (Foo.one == Foo.two)  // evaluates to falselet y = Foo.one.hashValue     // evaluates to 1
>>
>> Likewise, conformance to RawRepresentable is automatically derived for
>> enums with a raw type. Since there is precedent for derived conformances in
>> Swift, we propose extending this support to more value types.
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#proposed-solution>Proposed
>> solution
>>
>> In general, we propose that value types derive conformance to Equatable/
>> Hashable if all of its members are Equatable/Hashable. We describe the
>> specific conditions under which these conformances are derived below,
>> followed by the details of how the conformance requirements are implemented.
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#protocol-derivability-conditions>Protocol
>> derivability conditions
>>
>> For brevity, let P represent either the protocol Equatable or Hashable in
>> the descriptions below.
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#derived-conformances-for-enums>Derived
>> conformances for enums
>>
>> For an enum, derivability of P is based on the conformances of its
>> cases' associated values. Computed properties are not considered.
>>
>> The following rules determine whether conformance to P can be derived
>> for an enum:
>>
>>    -
>>
>>    An enum with no cases does not derive conformance to P, since it is
>>    not possible to create instances of such types.
>>    -
>>
>>    An enum with one or more cases derives conformance to P if all of the
>>    associated values of all of its cases conform to P (either explicitly
>>    or derived).
>>
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#derived-conformances-for-structs>Derived
>> conformances for structs
>>
>> For a struct, derivability of P is based on the conformances of its
>> stored instance properties *only*. Neither static properties nor
>> computed instance properties (those with custom getters) are considered.
>>
>> The following rules determine whether conformance to P can be derived
>> for a struct:
>>
>>    -
>>
>>    A struct with *no* stored properties does *not* derive conformance to
>>    P. (Even though it is vacuously true that all instances of a struct with
>>    no stored properties could be considered equal and hash to the same value,
>>    the reality is that such structs are more often used for
>>    grouping/nesting of other entities and not for their singular value, and we
>>    don't consider it worthwhile to generate extra code in this case.)
>>    -
>>
>>    A struct with one or more stored properties derives conformance to P if
>>    all if the types of all of its stored properties conform to P (either
>>    explicitly or derived).
>>
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#considerations-for-recursive-types>Considerations
>> for recursive types
>>
>> For brevity in the discussion below, the term *members* refers only to
>> those members that are checked for deriving conformances: *stored
>> properties* for structs and *associated values* for enums.
>>
>> Recursive value types require a bit more care when determining whether a
>> conformance can be derived. Consider the following enum with an indirect
>> case:
>>
>> enum TreeNode {
>>   case empty
>>   case leaf(value: Int)
>>   case internal(left: TreeNode, right: TreeNode)
>> }
>>
>> When examining the internal case, an application of the rules above
>> implies that "TreeNode derives P if TreeNode conforms to P"—a recursive
>> condition. In this situation, we note that any instance of this type—or of
>> any recursive type—forms a finite tree structure because the recursion must
>> be terminated eventually by using one of the other base cases. Therefore,
>> without changing the outcome, we can assume for the purposes of
>> determining whether T derives P that any members of type T already
>> conform to P. If any members of different types prohibit deriving P,
>> then we know that the whole type cannot derive it; likewise, if all of the
>> other members permit deriving P, then we know that T can derive it by
>> recursively applying the derived operation.
>>
>> This property can be extended to *mutually* recursive types as well.
>> Consider this contrived example:
>>
>> enum A {
>>   case value(Int)
>>   case b(B)
>> }
>> enum B {
>>   case value(String)
>>   case c(C)
>> }
>> enum C {
>>   case value(Double)
>>   case a(A)
>> }
>>
>> The rules state that—ignoring the trivial cases—"A derives P if B conforms
>> to P," and "B derives P if Cconforms to P," and "C derives P if A conforms
>> to P." The same observation about recursion and the finiteness of
>> instances from above holds here, so we can generalize the rule above as
>> follows:
>>
>> A type T can derive P if all members of T conform to P or are of types
>> found in cycles that lead back to Tsuch that the members of those other
>> types along the cycle also all conform to P or are themselves along such
>> a cycle.
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#other-considerations>Other
>> considerations
>>
>> When conditional conformances are supported in Swift, generic types
>> should conditionally derive Equatable and Hashable for type argument
>> substitutions where the rules above are satisfied.
>>
>> A notable side effect of this is that Optional<Wrapped> would derive
>> Equatable and Hashable conformance when Wrapped conforms to those
>> protocols, because it is an enum that would satisfy the rules described
>> above. Its implementation would not need to be in the standard library (but
>> there is also nothing preventing it from being there).
>>
>> Conditional conformances will also significantly improve derivability
>> coverage over other payload/member types. For example, consider a struct containing
>> a stored property that is an array of Equatable types:
>>
>> struct Foo {
>>   var values: [String]
>> }
>>
>> Today, Array<String> does not conform to Equatable, so its presence
>> would prohibit Foo from deriving Equatable. However, once Swift can
>> express the conformance Array<Element>: Equatable where Element:
>> Equatable, Foo would automatically derive Equatable as well. This makes
>> derived conformances significantly more powerful.
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#implementation-details>Implementation
>> details
>>
>> An enum T that derives Equatable will receive a compiler-generated
>> implementation of static == (lhs: T, rhs: T) -> Bool that returns true if
>> and only if lhs and rhs are the same case and have payloads that are
>> memberwise-equal.
>>
>> An enum T that derives Hashable will receive a compiler-generated
>> implementation of var hashValue: Int { get }that uses an unspecified
>> hash function† to compute the hash value by incorporating the case's
>> ordinal (i.e., definition order) followed by the hash values of its
>> associated values as its terms, also in definition order.
>>
>> A struct T that derives Equatable will receive a compiler-generated
>> implementation of static == (lhs: T, rhs: T) -> Bool that returns true if
>> and only if lhs.x == rhs.x for all stored properties in T.
>>
>> A struct T that derives Hashable will receive a compiler-generated
>> implementation of var hashValue: Int { get } that uses an unspecified
>> hash function† to compute the hash value by incorporating the hash
>> values of the fields as its terms, in definition order.
>>
>> † We intentionally leave the exact definition of the hash function
>> unspecified here. A multiplicative hash function with good distribution is
>> the likely candidate, but we do not rule out other possibilities. Users
>> should not depend on the nature of the generated implementation or rely on
>> particular outputs; we reserve the right to change it in the future.
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#overriding-derived-conformances>Overriding
>> derived conformances
>>
>> Any user-provided implementations of == or hashValue will override the
>> default implementations that would be provided by the compiler. This is
>> already the case possible today with raw-value enums so the same behavior
>> should be extended to other value types that are made to implicitly conform
>> to these protocols.
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#open-questions>Open
>> questions
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#omission-of-fields-from-generated-computations>Omission
>> of fields from generated computations
>>
>> Some commenters have expressed a desire to tag certain properties of a
>> struct from being included in automatically generated equality tests or
>> hash value computations. This could be valuable, for example, if a property
>> is merely used as an internal cache and does not actually contribute to the
>> "value" of the instance. Under the rules above, if this cached value was
>> equatable, a user would have to override == and hashValue and provide
>> their own implementations to ignore it.
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#explicit-or-implicit-derivation>Explicit
>> or implicit derivation
>>
>> As with raw-value enums today, should the derived conformance be
>> completely implicit, or should users have to explicitly list conformance
>> with Equatable and Hashable in order for the compiler to generate the
>> derived implementation?
>>
>> If derived conformances were made explicit, it could be argued that this
>> should also be done for consistency across raw-value enums as well. This
>> would be a source-breaking change, which should be avoided at this stage.
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#impact-on-existing-code>Impact
>> on existing code
>>
>> This change would make types that satisfy the rules above Equatable and
>> Hashable when they previously were not. It is not expected that there
>> would be any *behavioral* changes because of this; since Equatable and
>> Hashable have associated type requirements, users cannot be dynamically
>> testing for conformance to them at runtime.
>>
>> Value types that already provide custom implementations of Equatable and
>> Hashable would keep the custom implementation because it would override
>> the compiler-provided default.
>>
>> This change would potentially increase binary size when it generates
>> conformances that did not exist before, at least for types where it is not
>> possible to know that the conformances are unused and thus cannot be
>> dead-stripped (i.e., public types).
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#alternatives-considered>Alternatives
>> considered
>>
>> The original discussion thread also included Comparable as a candidate
>> for automatic generation. Unlike equatability and hashability, however,
>> comparability requires an ordering among the members being compared.
>> Automatically using the definition order here might be too surprising for
>> users, but worse, it also means that reordering properties in the source
>> code changes the code's behavior at runtime. (This is true for hashability
>> as well if a multiplicative hash function is used, but hash values are not
>> intended to be persistent and reordering the terms does not produce a
>> significant *behavioral* change.)
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#acknowledgments>
>> Acknowledgments
>>
>> Thanks to Joe Groff for spinning off the original discussion thread, Jose
>> Cheyo Jimenez for providing great real-world examples of boilerplate needed
>> to support equatability for some value types, and to Mark Sands for
>> necromancing the swift-evolution thread that convinced me to write this up.
>> ------------------------------
>>
>> <https://gist.github.com/allevato/2fd10290bfa84accfbe977d8ac07daad#rationale>
>> Rationale
>>
>> On [Date], the core team decided to (TBD) this proposal. When the core
>> team makes a decision regarding this proposal, their rationale for the
>> decision will be written here.
>>
>>
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170505/6e70ae20/attachment.html>


More information about the swift-evolution mailing list