[swift-evolution] [Draft] Automatically deriving Equatable and Hashable for certain value types

T.J. Usiyan griotspeak at gmail.com
Thu May 26 13:00:26 CDT 2016


A `deriving` keyword, at the very least, is pretty explicitly *not* an
all-or-nothing situation. If you want to define equality/hashability for
your type manually, don't use `deriving`. This should leave the simplest
cases to auto generation and anything more complex should be handled by the
developer.

On Thu, May 26, 2016 at 11:20 AM, L. Mihalkovic <
laurent.mihalkovic at gmail.com> wrote:

> what i care about is to have a choice about what DEFINES the identity of
> my values, not just an all-or-nothing situation.
>
> On May 26, 2016, at 5:18 PM, T.J. Usiyan via swift-evolution <
> swift-evolution at swift.org> wrote:
>
> +1 to a `deriving` keyword
>
> On Thu, May 26, 2016 at 3:58 AM, Michael Peternell via swift-evolution <
> swift-evolution at swift.org> wrote:
>
>> Can we just copy&paste the solution from Haskell instead of creating our
>> own? It's just better in every aspect. Deriving `Equatable` and `Hashable`
>> would become
>>
>> struct Polygon deriving Equatable, Hashable {
>>     ...
>> }
>>
>> This has several advantages:
>> - you don't have to guess wether `Equatable` or `Hashable` should be
>> automatically derived or not.
>> - Deriving becomes an explicit choice.
>> - If you need a custom `Equatable` implementation (for whatever reason),
>> you can still do it.
>> - It doesn't break any code that is unaware of the change
>> - It can be extended in future versions of Swift, without introducing any
>> new incompatibilities. For example, `CustomStringConvertible` could be
>> derived just as easily.
>> - It is compatible with generics. E.g. `struct Shape<T> deriving
>> Equatable` will make every `Shape<X>` equatable if `X` is equatable. But if
>> `X` is not equatable, `Shape<X>` can be used as well. (Unless `X` is not
>> used, in which case every `Shape<T>` would be equatable. Unless something
>> in the definition of `Shape` makes deriving `Equatable` impossible => this
>> produces an error.)
>> - It is proven to work in production.
>>
>> -Michael
>>
>> > Am 26.05.2016 um 03:48 schrieb Mark Sands via swift-evolution <
>> swift-evolution at swift.org>:
>> >
>> > Thanks so much for putting this together, Tony! Glad I was able to be
>> some inspiration. :^)
>> >
>> >
>> > On Wed, May 25, 2016 at 1:28 PM, Tony Allevato via swift-evolution <
>> swift-evolution at swift.org> wrote:
>> > I was inspired to put together a draft proposal based on an older
>> discussion in the Universal Equality, Hashability, and Comparability thread
>> <http://thread.gmane.org/gmane.comp.lang.swift.evolution/8919/> that
>> recently got necromanced (thanks Mark Sands!).
>> >
>> > I'm guessing that this would be a significant enough change that it's
>> not possible for the Swift 3 timeline, but it's something that would
>> benefit enough people that I want to make sure the discussion stays alive.
>> If there are enough good feelings about it, I'll move it from my gist into
>> an actual proposal PR.
>> >
>> > Automatically deriving Equatable andHashable for value types
>> >
>> >       • Proposal: SE-0000
>> >       • Author(s): Tony Allevato
>> >       • Status: Awaiting review
>> >       • Review manager: TBD
>> > 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 find themselves writing 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
>> toEquatable and Hashable to reduce this boilerplate, in a subset of
>> scenarios where generating the correct implementation is likely to be
>> possible.
>> >
>> > Swift-evolution thread: Universal Equatability, Hashability, and
>> Comparability
>> >
>> > Motivation
>> >
>> > Building robust value types in Swift can involve writing significant
>> boilerplate code to support concepts of 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:
>> >
>> > 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-valuedDictionary 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. One such real-world example (source):
>> >
>> > func ==(lhs: HandRank, rhs: HandRank) -> Bool
>> >  {
>> >
>> > switch
>> >  (lhs, rhs) {
>> >
>> > case (.straightFlush(let lRank, let lSuit), .straightFlush(let rRank ,
>> let
>> >  rSuit)):
>> >
>> > return lRank == rRank && lSuit ==
>> >  rSuit
>> >
>> > case (.fourOfAKind(four: let lFour), .fourOfAKind(four: let
>> >  rFour)):
>> >
>> > return lFour ==
>> >  rFour
>> >
>> > case (.fullHouse(three: let lThree), .fullHouse(three: let
>> >  rThree)):
>> >
>> > return lThree ==
>> >  rThree
>> >
>> > case (.flush(let lRank, let lSuit), .flush(let rRank, let
>> >  rSuit)):
>> >
>> > return lSuit == rSuit && lRank ==
>> >  rRank
>> >
>> > case (.straight(high: let lRank), .straight(high: let
>> >  rRank)):
>> >
>> > return lRank ==
>> >  rRank
>> >
>> > case (.threeOfAKind(three: let lRank), .threeOfAKind(three: let
>> >  rRank)):
>> >
>> > return lRank ==
>> >  rRank
>> >
>> > case (.twoPair(high: let lHigh, low: let lLow, highCard: let
>> >  lCard),
>> >
>> > .twoPair(high: let rHigh, low: let rLow, highCard: let
>> >  rCard)):
>> >
>> > return lHigh == rHigh && lLow == rLow && lCard ==
>> >  rCard
>> >
>> > case (.onePair(let lPairRank, card1: let lCard1, card2: let lCard2,
>> card3: let
>> >  lCard3),
>> >
>> > .onePair(let rPairRank, card1: let rCard1, card2: let rCard2, card3: let
>> >  rCard3)):
>> >
>> > return lPairRank == rPairRank && lCard1 == rCard1 && lCard2 == rCard2
>> && lCard3 ==
>> >  rCard3
>> >
>> > case (.highCard(let lCard), .highCard(let
>> >  rCard)):
>> >
>> > return lCard ==
>> >  rCard
>> >
>> > default
>> > :
>> >
>> > return false
>> >
>> >   }
>> > }
>> >
>> > Crafting a high-quality hash function for this enum would be similarly
>> inconvenient to write, involving another large switchstatement.
>> >
>> > Swift already provides implicit protocol conformance in some cases;
>> notably, enums with raw values conform toRawRepresentable, Equatable, and
>> Hashable without the user explicitly declaring them:
>> >
>> > enum Foo: Int
>> >  {
>> >
>> > case one = 1
>> >
>> >
>> > case two = 2
>> >
>> > }
>> >
>> >
>> > let x = (Foo.one == Foo.two)  // works
>> > let y = Foo.one.hashValue     // also works
>> > let z = Foo.one.rawValue      // also also works
>> > Since there is precedent for this in Swift, we propose extending this
>> support to more value types.
>> >
>> > Proposed solution
>> >
>> > We propose that a value type be Equatable/Hashable if all of its
>> members are Equatable/Hashable, with the result for the outer type being
>> composed from its members.
>> >
>> > Specifically, we propose the following rules for deriving Equatable:
>> >
>> >       • A struct implicitly conforms to Equatable if all of its fields
>> are of types that conform to Equatable – either explicitly, or implicitly
>> by the application of these rules. The compiler will generate an
>> implementation of ==(lhs: T, rhs: T)that returns true if and only if lhs.x
>> == rhs.x for all fields x in T.
>> >
>> >       • An enum implicitly conforms to Equatable if all of its
>> associated values across all of its cases are of types that conform to
>> Equatable – either explicitly, or implicitly by the application of these
>> rules. The compiler will generate an implementation of ==(lhs: T, rhs: T)
>> that returns true if and only if lhs and rhs are the same case and have
>> payloads that are memberwise-equal.
>> >
>> > Likewise, we propose the following rules for deriving Hashable:
>> >
>> >       • A struct implicitly conforms to Hashable if all of its fields
>> are of types that conform to Hashable – either explicitly, or implicitly by
>> the application of these rules. The compiler will generate an
>> implementation of hashValue that uses a pre-defined hash function† to
>> compute the hash value of the struct from the hash values of its members.
>> >
>> > Since order of the terms affects the hash value computation, we
>> recommend ordering the terms in member definition order.
>> >
>> >       • An enum implicitly conforms to Hashable if all of its
>> associated values across all of its cases are of types that conform to
>> Hashable – either explicitly, or implicitly by the application of these
>> rules. The compiler will generate an implementation of hashValue that uses
>> a pre-defined hash function† to compute the hash value of an enum value by
>> using the case's ordinal (i.e., definition order) followed by the hash
>> values of its associated values as its terms, also in definition order.
>> >
>> > † We leave the exact definition of the hash function unspecified here;
>> a multiplicative hash function such as Kernighan and Ritchie or Bernstein
>> is easy to implement, but we do not rule out other possibilities.
>> >
>> > Overriding defaults
>> >
>> > Any user-provided implementations of == or hashValue should override
>> the default implementations that would be provided by the compiler. This is
>> already 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.
>> >
>> > Open questions
>> >
>> > Omission of fields from generated computations
>> >
>> > Should it be possible to easily omit certain properties from
>> automatically generated equality tests or hash value computation? 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. If there is significant evidence that this pattern is common and
>> useful, we could consider adding a custom attribute, such as @transient,
>> that would omit the property from the generated computations.
>> >
>> > Explicit or implicit derivation
>> >
>> > As with raw-value enums today, should the derived conformance be
>> completely explicit, or should users have to explicitly list conformance
>> with Equatable and Hashable in order for the compiler to generate the
>> derived implementation?
>> >
>> > Impact on existing code
>> >
>> > This change will have no impact on existing code because it is purely
>> additive. Value types that already provide custom implementations of == or
>> hashValue but satisfy the rules above would keep the custom implementation
>> because it would override the compiler-provided default.
>> >
>> > 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.)
>> >
>> > 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.
>> >
>> >
>> > _______________________________________________
>> > 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
>>
>> _______________________________________________
>> 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/20160526/2c96e57f/attachment.html>


More information about the swift-evolution mailing list