Universal Equatability, Hashability, and Comparability


(Joe Groff) #1

(starting a new thread by DaveA's request)

There's a definition of equality that makes sense as a default for nearly every type in our system:

- Basic types like IntNN, FloatNN, String, etc. have domain-defined equality,
- Structs and tuples can be considered equal if their corresponding fields are equal,
- Enums can be considered equal if they carry the same, equal payload,
- Class references can be considered equal if they refer to the same instance,
- Metatypes can be considered equal if they represent the same type, and
- Existentials can be considered equal if they carry equal values of the same dynamic type.

and similarly, reasonable hash code implementations could be synthesized by applying a standard hash combine operation over the components, and a default ordering could be assigned to values of every type. I think it's worth considering whether Equatable, Hashable, and/or Comparable, instead of being explicit protocols, should become universal behavior like 'print', with customization points to override the default behavior. If Equatable and Hashable behavior were universal, that would solve many of the common problems people currently have trying to work with heterogeneous containers. In object-oriented frameworks, including Cocoa, Java, and .NET, it is common for the root (NS)Object class to provide default equality and hashing operations. There are of course some tradeoffs:

- Universal behavior would require us to either generate code for '==', 'hashValue', and/or '<' for every type, or provide sufficient reflection info for a common runtime implementation to do it. The reflection-based approach may be reasonable for print(), since dumping reflection info only reduces the quality of the default logging behavior, but '==' and 'hashValue' are more essential to proper behavior, so relying on reflection might be too slow, and would be brittle when we introduce the ability to drop reflection info.
- Type safety with '==' is important to prevent accidental '1 == "1"' type comparsions, and a fully generic 'func ==<T>(x: T, y: T) -> Bool' could potentially allow those sorts of mixed-type comparisons by accident. Language rules that constrained when generic parameters can be resolved to supertypes might help here.
- Function types in Swift do not provide a ready equality operation. We could provide a default implementation that always returns 'false', perhaps.
- A Comparable ordering can be dreamt up for many types, but it's not always a stable ordering, or a desired one. Many people have complained that 'nil < .Some(1)' works for optionals, for instance, ordering 'nil' below Some values. We could use pointer identity to order class instances and types, but this wouldn't be a stable ordering across process runs. That might be good enough for ordered collections like search trees, but is weaker than what many people expect '<' to do.

It's my feeling that Equatable and Hashable would make a lot of sense as universal operations; I'm not so sure about Comparable.

-Joe


#2

Definitely a +1 on the basics. When you get inheritance involved, does that
complicates things a little bit?

Let's say I have a subclass instance that has corresponding fields with a
superclass instance. Is it equal to said super-class instance using just
member-wise comparisons? Would that be problematic? In Scala you'd often
use a reference to an "equality contract" object type in order to get
"transitive" equality between subclasses and superclasses, which definitely
feels like a step backwards from the current protocol-driven approach.

···

On Tue, Mar 8, 2016 at 2:54 PM, Joe Groff via swift-evolution < swift-evolution@swift.org> wrote:

(starting a new thread by DaveA's request)

There's a definition of equality that makes sense as a default for nearly
every type in our system:

- Basic types like IntNN, FloatNN, String, etc. have domain-defined
equality,
- Structs and tuples can be considered equal if their corresponding fields
are equal,
- Enums can be considered equal if they carry the same, equal payload,
- Class references can be considered equal if they refer to the same
instance,
- Metatypes can be considered equal if they represent the same type, and
- Existentials can be considered equal if they carry equal values of the
same dynamic type.

and similarly, reasonable hash code implementations could be synthesized
by applying a standard hash combine operation over the components, and a
default ordering could be assigned to values of every type. I think it's
worth considering whether Equatable, Hashable, and/or Comparable, instead
of being explicit protocols, should become universal behavior like 'print',
with customization points to override the default behavior. If Equatable
and Hashable behavior were universal, that would solve many of the common
problems people currently have trying to work with heterogeneous
containers. In object-oriented frameworks, including Cocoa, Java, and .NET,
it is common for the root (NS)Object class to provide default equality and
hashing operations. There are of course some tradeoffs:

- Universal behavior would require us to either generate code for '==',
'hashValue', and/or '<' for every type, or provide sufficient reflection
info for a common runtime implementation to do it. The reflection-based
approach may be reasonable for print(), since dumping reflection info only
reduces the quality of the default logging behavior, but '==' and
'hashValue' are more essential to proper behavior, so relying on reflection
might be too slow, and would be brittle when we introduce the ability to
drop reflection info.
- Type safety with '==' is important to prevent accidental '1 == "1"' type
comparsions, and a fully generic 'func ==<T>(x: T, y: T) -> Bool' could
potentially allow those sorts of mixed-type comparisons by accident.
Language rules that constrained when generic parameters can be resolved to
supertypes might help here.
- Function types in Swift do not provide a ready equality operation. We
could provide a default implementation that always returns 'false', perhaps.
- A Comparable ordering can be dreamt up for many types, but it's not
always a stable ordering, or a desired one. Many people have complained
that 'nil < .Some(1)' works for optionals, for instance, ordering 'nil'
below Some values. We could use pointer identity to order class instances
and types, but this wouldn't be a stable ordering across process runs. That
might be good enough for ordered collections like search trees, but is
weaker than what many people expect '<' to do.

It's my feeling that Equatable and Hashable would make a lot of sense as
universal operations; I'm not so sure about Comparable.

-Joe
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brent Royal-Gordon) #3

- Function types in Swift do not provide a ready equality operation. We could provide a default implementation that always returns 'false', perhaps.

I think this sort of puts the lie to the idea.

We can always provide *a* definition of equality, but I suspect it will often be an *incorrect* definition. That's why you had to suggest functions should always be false: you cannot (without more effort than you want to spend) provide a correct definition of equality for it.

I mean, imagine what happens if you make functions Equatable and Hashable but with definitions that don't actually work. Currently, `Set<Void -> Void>` gives you an error:

  error: type 'Void -> Void' does not conform to protocol 'Hashable'

But with this feature in place, Swift would happily produce a Set of functions which collides endlessly, doesn't do any uniquing, never says it contains any value you pass into it, and can only remove elements by index. A type that is "never equal" completely breaks Set in practice, and there's no way for the type system to catch the problem.

If we automatically synthesize a == operator for every type, many of those operators will be incorrect. For instance, anything that includes a cache will be incorrect. Anything that includes a pointer to a buffer and ought to evaluate the buffer's contents will be incorrect. Anything that includes a closure will be incorrect. Individually, each of these cases is minor, but they multiply and interact with each other until, together, they undermine confidence in ==.

If you explicitly mark things as Equatable, then it is clear that the equality operator on them really does have a sensible definition. But if you can pass anything into ==, you will never know what will actually *work*. If everything's Equatable, then nothing is.

* * *

Auto-deriving is a different story, though, especially if it's opt-in (you have to say `deriving Equatable`). There, you presumably have looked at the default semantics and determined they're appropriate for your type.

But I think it's clear that derived conformances should eventually be a user-accessible feature. We already derive RawRepresentable and ErrorType on enums. (We also derive initializers on some types, but that's arguably a separate feature.) I think it's clear that Swift ∞ ought to allow you to derive protocol conformances; it's just a matter of scheduling.

So I think that what we ought to do is this:

• Make a best guess at what Swift ∞ would want you to do to invoke the user-specified derivation logic for an arbitrary protocol—implicit derivation or something marked by a keyword.
• Think about whether derived conformances of Equatable, Hashable, and/or Comparable are urgent enough that we should implement them before the general feature.
• If so, design these features along the lines of what we would expect the eventual user-specified derivation feature to use.

···

--
Brent Royal-Gordon
Architechies


(William Dillon) #4

I’m +1 to this idea for Hashable and Equatable.

I really hate writing hashValue implementations, and equality is something that I wouldn’t miss. The way Joe proposes these would be implemented is already very close to what I do anyway. I don’t see how it would have a negative effect on existing code. Existing code that uses ‘==‘ or ‘hashValue' necessarily implements these functions, and they would override the default implementation.

It seems to me that the biggest risk would be if you try to compare two instances (using ==, for example) and you haven’t overridden the default implementation. If you have a weird type, and get unexpected behavior, it may be hard to remember that ‘==‘ might be to blame. With the current status quo, Swift would just say “no” and you’d be on your way to writing a new implementation. With this proposal, it might take a bit more time to understand what’s happening. However, I doubt that the comparison of (really?) weird types are very frequent. Furthermore, if you do have something that is that weird, as you’re typing “==“ you’re probably thinking “This is a really weird type, I should probably make sure that the default ‘==‘ makes sense.”

In my opinion, making it opt-in would introduce relatively weird or inelegant syntax to mark your type as opting into the behavior. I’d rather allow for overridden behavior.

Finally, I don’t think Comparable is a good idea for this.

- Will

···

On Mar 8, 2016, at 12:54 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

(starting a new thread by DaveA's request)

There's a definition of equality that makes sense as a default for nearly every type in our system:

- Basic types like IntNN, FloatNN, String, etc. have domain-defined equality,
- Structs and tuples can be considered equal if their corresponding fields are equal,
- Enums can be considered equal if they carry the same, equal payload,
- Class references can be considered equal if they refer to the same instance,
- Metatypes can be considered equal if they represent the same type, and
- Existentials can be considered equal if they carry equal values of the same dynamic type.

and similarly, reasonable hash code implementations could be synthesized by applying a standard hash combine operation over the components, and a default ordering could be assigned to values of every type. I think it's worth considering whether Equatable, Hashable, and/or Comparable, instead of being explicit protocols, should become universal behavior like 'print', with customization points to override the default behavior. If Equatable and Hashable behavior were universal, that would solve many of the common problems people currently have trying to work with heterogeneous containers. In object-oriented frameworks, including Cocoa, Java, and .NET, it is common for the root (NS)Object class to provide default equality and hashing operations. There are of course some tradeoffs:

- Universal behavior would require us to either generate code for '==', 'hashValue', and/or '<' for every type, or provide sufficient reflection info for a common runtime implementation to do it. The reflection-based approach may be reasonable for print(), since dumping reflection info only reduces the quality of the default logging behavior, but '==' and 'hashValue' are more essential to proper behavior, so relying on reflection might be too slow, and would be brittle when we introduce the ability to drop reflection info.
- Type safety with '==' is important to prevent accidental '1 == "1"' type comparsions, and a fully generic 'func ==<T>(x: T, y: T) -> Bool' could potentially allow those sorts of mixed-type comparisons by accident. Language rules that constrained when generic parameters can be resolved to supertypes might help here.
- Function types in Swift do not provide a ready equality operation. We could provide a default implementation that always returns 'false', perhaps.
- A Comparable ordering can be dreamt up for many types, but it's not always a stable ordering, or a desired one. Many people have complained that 'nil < .Some(1)' works for optionals, for instance, ordering 'nil' below Some values. We could use pointer identity to order class instances and types, but this wouldn't be a stable ordering across process runs. That might be good enough for ordered collections like search trees, but is weaker than what many people expect '<' to do.

It's my feeling that Equatable and Hashable would make a lot of sense as universal operations; I'm not so sure about Comparable.

-Joe
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Dave Abrahams) #5

(starting a new thread by DaveA's request)

There's a definition of equality that makes sense as a default for nearly every type in our system:

- Basic types like IntNN, FloatNN, String, etc. have domain-defined
equality,

But floats, as ever, are weird because their domain equality isn't an
equivalence relation. Just one of the many little issues to be worked
out ;-).

- Structs and tuples can be considered equal if their corresponding fields are equal,
- Enums can be considered equal if they carry the same, equal payload,
- Class references can be considered equal if they refer to the same instance,
- Metatypes can be considered equal if they represent the same type, and
- Existentials can be considered equal if they carry equal values of the same dynamic type.

and similarly, reasonable hash code implementations could be
synthesized by applying a standard hash combine operation over the
components, and a default ordering could be assigned to values of
every type. I think it's worth considering whether Equatable,
Hashable, and/or Comparable, instead of being explicit protocols,
should become universal behavior like 'print', with customization
points to override the default behavior.

Yes, please, let's consider that. If the default behavior is going to
be a reasonable correct behavior 95% of the time (which IMO it is), this
will increase interoperability and decrease boilerplate.

I think because of the tight relationship between ==, copying, and
assignment, this discussion probably drags in the issues of:

* how to constrain generic parameters to having value semantics
* how to universally clone/assign mutable instances
* one more I thought of that slipped my mind; it'll come back to me.

If Equatable and Hashable behavior were universal, that would solve
many of the common problems people currently have trying to work with
heterogeneous containers. In object-oriented frameworks, including
Cocoa, Java, and .NET, it is common for the root (NS)Object class to
provide default equality and hashing operations. There are of course
some tradeoffs:

- Universal behavior would require us to either generate code for
'==', 'hashValue', and/or '<' for every type, or provide sufficient
reflection info for a common runtime implementation to do it. The
reflection-based approach may be reasonable for print(), since dumping
reflection info only reduces the quality of the default logging
behavior, but '==' and 'hashValue' are more essential to proper
behavior, so relying on reflection might be too slow, and would be
brittle when we introduce the ability to drop reflection info.
- Type safety with '==' is important to prevent accidental '1 == "1"'
type comparsions, and a fully generic 'func ==<T>(x: T, y: T) -> Bool'
could potentially allow those sorts of mixed-type comparisons by
accident. Language rules that constrained when generic parameters can
be resolved to supertypes might help here.
- Function types in Swift do not provide a ready equality
operation. We could provide a default implementation that always
returns 'false', perhaps.

Like Float equality, that's not an equivalence relation, only this
one is more serious, because for most Floats you can pretend it's an
equivalence relation unless you run into NaN. I'm unsure what to do
about these two, but they bear careful consideration.

- A Comparable ordering can be dreamt up for many types, but it's not
always a stable ordering,

What do you mean by stable ordering?

or a desired one.

That's true. But what's important is that it's consistent with == and
allows lookup in a sorted container. The core ordering operation should
not be spelled "<" though; we should use "<=>" for that so that types
can keep their domain-specific "<" if necessary. Too bad the same trick
doesn't work for "==" :-).

Many people have complained that 'nil < .Some(1)' works for optionals,
for instance, ordering 'nil' below Some values. We could use pointer
identity to order class instances and types, but this wouldn't be a
stable ordering across process runs.

Oh, that's what you mean by "stable." So what?

That might be good enough for ordered collections like search trees,
but is weaker than what many people expect '<' to do.

MMmph. I don't think it's unreasonable to say that if your expectations
don't match the defaults, you can define your own.

···

on Tue Mar 08 2016, Joe Groff <jgroff-AT-apple.com> wrote:

--
-Dave


(Chéyo Jiménez) #6

(starting a new thread by DaveA's request)

There's a definition of equality that makes sense as a default for nearly every type in our system:

- Basic types like IntNN, FloatNN, String, etc. have domain-defined equality,
- Structs and tuples can be considered equal if their corresponding fields are equal,
- Enums can be considered equal if they carry the same, equal payload,

+1 for Equality, Comparable and Hashable especially for enums and tuples.

I know that equality was added for tuples but I think that enums are perfect for adding automatic generation of conformance to Equality, Comparable and Hashable.

Take a look at this toy example of an Enum with 9 cases

boiler plate to conform to Equatable
https://github.com/exercism/xswift/blob/master/exercises/poker/PokerExample.swift#L151-L189

boiler plate to conform to Comparable
https://github.com/exercism/xswift/blob/master/exercises/poker/PokerExample.swift#L191-L245

It's my feeling that Equatable and Hashable would make a lot of sense as universal operations; I'm not so sure about Comparable.

-Joe

For enums a defaultComparable protocol could just rank on the order the fields are declared:
https://github.com/exercism/xswift/blob/master/exercises/poker/PokerExample.swift#L211-L212


(Austin Zheng) #7

I would prefer Equatable and Hashable to remain opt-in, and for us to add
better support for automatic deriving of implementation.

For something like printing the representation of an object to a string,
there exists a "not wrong" mapping of every possible value to a string.
That is, if my FooStruct doesn't provide a custom description, having the
runtime convert it to something like "(FooStruct instance)" is still a
valid mapping. It might not be useful, but it's not wrong.

I don't think the same applies for equatability. The universal default
behavior for equating two objects is either correct or incorrect, and it's
not possible to know beforehand which is which. One of the wonderful things
about the current Swift system is that (modulo some exceptional cases) only
things explicitly meant to be equatable with each other are comparable. We
avoid the object-oriented pitfall in which 'equality' means two different
things - equality of value if you implemented an override properly; a
default 'equality of instance' otherwise (which might be right or wrong).
Of course, the same pitfall wouldn't necessarily apply in our case, but the
problem of having a 'default' == impl that allows a developer to falsely
assume their type is being properly compared (or not think about it at all)
would still be present.

Best,
Austin

···

On Tue, Mar 8, 2016 at 2:02 PM, Brian Pratt via swift-evolution < swift-evolution@swift.org> wrote:

Definitely a +1 on the basics. When you get inheritance involved, does
that complicates things a little bit?

Let's say I have a subclass instance that has corresponding fields with a
superclass instance. Is it equal to said super-class instance using just
member-wise comparisons? Would that be problematic? In Scala you'd often
use a reference to an "equality contract" object type in order to get
"transitive" equality between subclasses and superclasses, which definitely
feels like a step backwards from the current protocol-driven approach.

On Tue, Mar 8, 2016 at 2:54 PM, Joe Groff via swift-evolution < > swift-evolution@swift.org> wrote:

(starting a new thread by DaveA's request)

There's a definition of equality that makes sense as a default for nearly
every type in our system:

- Basic types like IntNN, FloatNN, String, etc. have domain-defined
equality,
- Structs and tuples can be considered equal if their corresponding
fields are equal,
- Enums can be considered equal if they carry the same, equal payload,
- Class references can be considered equal if they refer to the same
instance,
- Metatypes can be considered equal if they represent the same type, and
- Existentials can be considered equal if they carry equal values of the
same dynamic type.

and similarly, reasonable hash code implementations could be synthesized
by applying a standard hash combine operation over the components, and a
default ordering could be assigned to values of every type. I think it's
worth considering whether Equatable, Hashable, and/or Comparable, instead
of being explicit protocols, should become universal behavior like 'print',
with customization points to override the default behavior. If Equatable
and Hashable behavior were universal, that would solve many of the common
problems people currently have trying to work with heterogeneous
containers. In object-oriented frameworks, including Cocoa, Java, and .NET,
it is common for the root (NS)Object class to provide default equality and
hashing operations. There are of course some tradeoffs:

- Universal behavior would require us to either generate code for '==',
'hashValue', and/or '<' for every type, or provide sufficient reflection
info for a common runtime implementation to do it. The reflection-based
approach may be reasonable for print(), since dumping reflection info only
reduces the quality of the default logging behavior, but '==' and
'hashValue' are more essential to proper behavior, so relying on reflection
might be too slow, and would be brittle when we introduce the ability to
drop reflection info.
- Type safety with '==' is important to prevent accidental '1 == "1"'
type comparsions, and a fully generic 'func ==<T>(x: T, y: T) -> Bool'
could potentially allow those sorts of mixed-type comparisons by accident.
Language rules that constrained when generic parameters can be resolved to
supertypes might help here.
- Function types in Swift do not provide a ready equality operation. We
could provide a default implementation that always returns 'false', perhaps.
- A Comparable ordering can be dreamt up for many types, but it's not
always a stable ordering, or a desired one. Many people have complained
that 'nil < .Some(1)' works for optionals, for instance, ordering 'nil'
below Some values. We could use pointer identity to order class instances
and types, but this wouldn't be a stable ordering across process runs. That
might be good enough for ordered collections like search trees, but is
weaker than what many people expect '<' to do.

It's my feeling that Equatable and Hashable would make a lot of sense as
universal operations; I'm not so sure about Comparable.

-Joe
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Jordan Rose) #8

Agreed. There are plenty of systems where == on classes means === by default, and then people forget to override == when they're making a class type that doesn't need identity.

(Then again, should such a type be wrapped in a value type in Swift, to communicate that it doesn't use identity? But then that type will derive ==.)

Making them derivable seems totally reasonable. Today just declaring Equatable or Hashable is enough to do that in the few places where we do derive conformances; we could either do that or invent a new "deriving" syntax.

Jordan

···

On Mar 8, 2016, at 14:15, Austin Zheng via swift-evolution <swift-evolution@swift.org> wrote:

I would prefer Equatable and Hashable to remain opt-in, and for us to add better support for automatic deriving of implementation.

For something like printing the representation of an object to a string, there exists a "not wrong" mapping of every possible value to a string. That is, if my FooStruct doesn't provide a custom description, having the runtime convert it to something like "(FooStruct instance)" is still a valid mapping. It might not be useful, but it's not wrong.

I don't think the same applies for equatability. The universal default behavior for equating two objects is either correct or incorrect, and it's not possible to know beforehand which is which. One of the wonderful things about the current Swift system is that (modulo some exceptional cases) only things explicitly meant to be equatable with each other are comparable. We avoid the object-oriented pitfall in which 'equality' means two different things - equality of value if you implemented an override properly; a default 'equality of instance' otherwise (which might be right or wrong). Of course, the same pitfall wouldn't necessarily apply in our case, but the problem of having a 'default' == impl that allows a developer to falsely assume their type is being properly compared (or not think about it at all) would still be present.

Best,
Austin

On Tue, Mar 8, 2016 at 2:02 PM, Brian Pratt via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
Definitely a +1 on the basics. When you get inheritance involved, does that complicates things a little bit?

Let's say I have a subclass instance that has corresponding fields with a superclass instance. Is it equal to said super-class instance using just member-wise comparisons? Would that be problematic? In Scala you'd often use a reference to an "equality contract" object type in order to get "transitive" equality between subclasses and superclasses, which definitely feels like a step backwards from the current protocol-driven approach.

On Tue, Mar 8, 2016 at 2:54 PM, Joe Groff via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
(starting a new thread by DaveA's request)

There's a definition of equality that makes sense as a default for nearly every type in our system:

- Basic types like IntNN, FloatNN, String, etc. have domain-defined equality,
- Structs and tuples can be considered equal if their corresponding fields are equal,
- Enums can be considered equal if they carry the same, equal payload,
- Class references can be considered equal if they refer to the same instance,
- Metatypes can be considered equal if they represent the same type, and
- Existentials can be considered equal if they carry equal values of the same dynamic type.

and similarly, reasonable hash code implementations could be synthesized by applying a standard hash combine operation over the components, and a default ordering could be assigned to values of every type. I think it's worth considering whether Equatable, Hashable, and/or Comparable, instead of being explicit protocols, should become universal behavior like 'print', with customization points to override the default behavior. If Equatable and Hashable behavior were universal, that would solve many of the common problems people currently have trying to work with heterogeneous containers. In object-oriented frameworks, including Cocoa, Java, and .NET, it is common for the root (NS)Object class to provide default equality and hashing operations. There are of course some tradeoffs:

- Universal behavior would require us to either generate code for '==', 'hashValue', and/or '<' for every type, or provide sufficient reflection info for a common runtime implementation to do it. The reflection-based approach may be reasonable for print(), since dumping reflection info only reduces the quality of the default logging behavior, but '==' and 'hashValue' are more essential to proper behavior, so relying on reflection might be too slow, and would be brittle when we introduce the ability to drop reflection info.
- Type safety with '==' is important to prevent accidental '1 == "1"' type comparsions, and a fully generic 'func ==<T>(x: T, y: T) -> Bool' could potentially allow those sorts of mixed-type comparisons by accident. Language rules that constrained when generic parameters can be resolved to supertypes might help here.
- Function types in Swift do not provide a ready equality operation. We could provide a default implementation that always returns 'false', perhaps.
- A Comparable ordering can be dreamt up for many types, but it's not always a stable ordering, or a desired one. Many people have complained that 'nil < .Some(1)' works for optionals, for instance, ordering 'nil' below Some values. We could use pointer identity to order class instances and types, but this wouldn't be a stable ordering across process runs. That might be good enough for ordered collections like search trees, but is weaker than what many people expect '<' to do.

It's my feeling that Equatable and Hashable would make a lot of sense as universal operations; I'm not so sure about Comparable.

-Joe
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Zachary Waldowski) #9

I completely agree with Austin here. Automatic derivation (perhaps
through the same mechanisms Joe is talking about) would be a nice
enhancement, but I find it refreshing and advantageous for simple value
types to have very little automatic behavior.

Cheers! Zachary Waldowski zach@waldowski.me

I would prefer Equatable and Hashable to remain opt-in, and for us to
add better support for automatic deriving of implementation.

For something like printing the representation of an object to a
string, there exists a "not wrong" mapping of every possible value to
a string. That is, if my FooStruct doesn't provide a custom
description, having the runtime convert it to something like
"(FooStruct instance)" is still a valid mapping. It might not be
useful, but it's not wrong.

I don't think the same applies for equatability. The universal default
behavior for equating two objects is either correct or incorrect, and
it's not possible to know beforehand which is which. One of the
wonderful things about the current Swift system is that (modulo some
exceptional cases) only things explicitly meant to be equatable with
each other are comparable. We avoid the object-oriented pitfall in
which 'equality' means two different things - equality of value if you
implemented an override properly; a default 'equality of instance'
otherwise (which might be right or wrong). Of course, the same pitfall
wouldn't necessarily apply in our case, but the problem of having a
'default' == impl that allows a developer to falsely assume their type
is being properly compared (or not think about it at all) would still
be present.

Best, Austin

Definitely a +1 on the basics. When you get inheritance involved,
does that complicates things a little bit?

Let's say I have a subclass instance that has corresponding fields
with a superclass instance. Is it equal to said super-class instance
using just member-wise comparisons? Would that be problematic? In
Scala you'd often use a reference to an "equality contract" object
type in order to get "transitive" equality between subclasses and
superclasses, which definitely feels like a step backwards from the
current protocol-driven approach.

(starting a new thread by DaveA's request)

There's a definition of equality that makes sense as a default for
nearly every type in our system:

- Basic types like IntNN, FloatNN, String, etc. have domain-
  defined equality,

- Structs and tuples can be considered equal if their corresponding
  fields are equal,

- Enums can be considered equal if they carry the same, equal payload,

- Class references can be considered equal if they refer to the
  same instance,

- Metatypes can be considered equal if they represent the same type, and

- Existentials can be considered equal if they carry equal values of the
  same dynamic type.

and similarly, reasonable hash code implementations could be synthesized
by applying a standard hash combine operation over the components, and a
default ordering could be assigned to values of every type. I think
it's worth considering whether Equatable, Hashable, and/or Comparable,
instead of being explicit protocols, should become universal behavior
like 'print', with customization points to override the default
behavior. If Equatable and Hashable behavior were universal, that would
solve many of the common problems people currently have trying to work
with heterogeneous containers. In object-oriented frameworks, including
Cocoa, Java, and .NET, it is common for the root (NS)Object class to
provide default equality and hashing operations. There are of course
some tradeoffs:

- Universal behavior would require us to either generate code for '==',
  'hashValue', and/or '<' for every type, or provide sufficient
  reflection info for a common runtime implementation to do it. The reflection-
  based approach may be reasonable for print(), since dumping reflection
  info only reduces the quality of the default logging behavior, but
  '==' and 'hashValue' are more essential to proper behavior, so relying
  on reflection might be too slow, and would be brittle when we
  introduce the ability to drop reflection info.

- Type safety with '==' is important to prevent accidental '1 == "1"'
  type comparsions, and a fully generic 'func ==<T>(x: T, y: T) -> Bool'
  could potentially allow those sorts of mixed-type comparisons by
  accident. Language rules that constrained when generic parameters can
  be resolved to supertypes might help here.

- Function types in Swift do not provide a ready equality operation.
  We could provide a default implementation that always returns
  'false', perhaps.

- A Comparable ordering can be dreamt up for many types, but it's not
  always a stable ordering, or a desired one. Many people have
  complained that 'nil < .Some(1)' works for optionals, for instance,
  ordering 'nil' below Some values. We could use pointer identity to
  order class instances and types, but this wouldn't be a stable
  ordering across process runs. That might be good enough for ordered
  collections like search trees, but is weaker than what many people
  expect '<' to do.

It's my feeling that Equatable and Hashable would make a lot of sense as
universal operations; I'm not so sure about Comparable.

-Joe

···

On Tue, Mar 8, 2016, at 05:15 PM, Austin Zheng via swift-evolution wrote:

On Tue, Mar 8, 2016 at 2:02 PM, Brian Pratt via swift-evolution <swift- > evolution@swift.org> wrote:

On Tue, Mar 8, 2016 at 2:54 PM, Joe Groff via swift-evolution <swift- >> evolution@swift.org> wrote:

_______________________________________________

swift-evolution mailing list

swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________

swift-evolution mailing list

swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_________________________________________________
swift-evolution mailing list swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(David Hart) #10

I would prefer Equatable and Hashable to remain opt-in, and for us to add better support for automatic deriving of implementation.

I completely agree with Austin here. Automatic derivation (perhaps through the same mechanisms Joe is talking about) would be a nice enhancement, but I find it refreshing and advantageous for simple value types to have very little automatic behavior.

Pedantically I agree with both of you, but from a very pragmatic point of you, I think it's very important to point out what Joe said about how this could reduce one of the most frustrating aspects of Swift, when people work with heterogeneous arrays and try to conform to Equatable:

···

On 08 Mar 2016, at 23:15, Austin Zheng via swift-evolution <swift-evolution@swift.org> wrote:
On 08 Mar 2016, at 23:57, Zach Waldowski via swift-evolution <swift-evolution@swift.org> wrote:

that would solve many of the common problems people currently have trying to work with heterogeneous containers.


(Russ Bishop) #11

- Function types in Swift do not provide a ready equality operation. We could provide a default implementation that always returns 'false', perhaps.

I think this sort of puts the lie to the idea.

We can always provide *a* definition of equality, but I suspect it will often be an *incorrect* definition. That's why you had to suggest functions should always be false: you cannot (without more effort than you want to spend) provide a correct definition of equality for it.

+1 to this. Having come from years of writing C# code, having every type implement Equatable and Hashable just led to a lot of useless magic because there are different definitions of equality and the default implementation was often not what you wanted, or your implementation was accidentally broken but you didn’t realize it.

* * *

Auto-deriving is a different story, though, especially if it's opt-in (you have to say `deriving Equatable`). There, you presumably have looked at the default semantics and determined they're appropriate for your type.

But I think it's clear that derived conformances should eventually be a user-accessible feature. We already derive RawRepresentable and ErrorType on enums. (We also derive initializers on some types, but that's arguably a separate feature.) I think it's clear that Swift ∞ ought to allow you to derive protocol conformances; it's just a matter of scheduling.

So I think that what we ought to do is this:

• Make a best guess at what Swift ∞ would want you to do to invoke the user-specified derivation logic for an arbitrary protocol—implicit derivation or something marked by a keyword.
• Think about whether derived conformances of Equatable, Hashable, and/or Comparable are urgent enough that we should implement them before the general feature.
• If so, design these features along the lines of what we would expect the eventual user-specified derivation feature to use.

We can also allow opt-in by adopting a protocol, eg `struct SimpleStruct: DefaultEquatable`. DefaultEquatable would tell the compiler to auto-generate an equatable implementation. Similar for DefaultHashable.

In the C# world you’d actually do something like this with an attribute. I know user-defined attributes aren’t on the table right now but some more built-in ones would be sufficient: @defaultEquatable, @defaultHashable, etc.
They could even take a list of properties to ignore if such a feature were useful: `@defaultEquatable(ignored: [currentDate, randomInt])`.

Russ

···

On Mar 8, 2016, at 5:12 PM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:


(Dave Abrahams) #12

Definitely a +1 on the basics. When you get inheritance involved, does that
complicates things a little bit?

Let's say I have a subclass instance that has corresponding fields with a
superclass instance. Is it equal to said super-class instance using just
member-wise comparisons? Would that be problematic?

No and yes, most of the time.

The default for classes would be to compare identity; problem solved :-).

···

on Tue Mar 08 2016, Brian Pratt <brian-AT-pratt.io> wrote:

In Scala you'd often use a reference to an "equality contract" object
type in order to get "transitive" equality between subclasses and
superclasses, which definitely feels like a step backwards from the
current protocol-driven approach.

On Tue, Mar 8, 2016 at 2:54 PM, Joe Groff via swift-evolution < > swift-evolution@swift.org> wrote:

(starting a new thread by DaveA's request)

There's a definition of equality that makes sense as a default for nearly
every type in our system:

- Basic types like IntNN, FloatNN, String, etc. have domain-defined
equality,
- Structs and tuples can be considered equal if their corresponding fields
are equal,
- Enums can be considered equal if they carry the same, equal payload,
- Class references can be considered equal if they refer to the same
instance,
- Metatypes can be considered equal if they represent the same type, and
- Existentials can be considered equal if they carry equal values of the
same dynamic type.

and similarly, reasonable hash code implementations could be synthesized
by applying a standard hash combine operation over the components, and a
default ordering could be assigned to values of every type. I think it's
worth considering whether Equatable, Hashable, and/or Comparable, instead
of being explicit protocols, should become universal behavior like 'print',
with customization points to override the default behavior. If Equatable
and Hashable behavior were universal, that would solve many of the common
problems people currently have trying to work with heterogeneous
containers. In object-oriented frameworks, including Cocoa, Java, and .NET,
it is common for the root (NS)Object class to provide default equality and
hashing operations. There are of course some tradeoffs:

- Universal behavior would require us to either generate code for '==',
'hashValue', and/or '<' for every type, or provide sufficient reflection
info for a common runtime implementation to do it. The reflection-based
approach may be reasonable for print(), since dumping reflection info only
reduces the quality of the default logging behavior, but '==' and
'hashValue' are more essential to proper behavior, so relying on reflection
might be too slow, and would be brittle when we introduce the ability to
drop reflection info.
- Type safety with '==' is important to prevent accidental '1 == "1"' type
comparsions, and a fully generic 'func ==<T>(x: T, y: T) -> Bool' could
potentially allow those sorts of mixed-type comparisons by accident.
Language rules that constrained when generic parameters can be resolved to
supertypes might help here.
- Function types in Swift do not provide a ready equality operation. We
could provide a default implementation that always returns 'false', perhaps.
- A Comparable ordering can be dreamt up for many types, but it's not
always a stable ordering, or a desired one. Many people have complained
that 'nil < .Some(1)' works for optionals, for instance, ordering 'nil'
below Some values. We could use pointer identity to order class instances
and types, but this wouldn't be a stable ordering across process runs. That
might be good enough for ordered collections like search trees, but is
weaker than what many people expect '<' to do.

It's my feeling that Equatable and Hashable would make a lot of sense as
universal operations; I'm not so sure about Comparable.

-Joe
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
-Dave


(Joe Groff) #13

What if we separated "ordering for a collection" from the < == > family altogether? Even Floats can be given a true equivalence relationship and assigned a stable total ordering for the purposes of container identity. I think it's reasonable for types to be able to provide an abstract ordering without making '<' and friends casually work. Programmers may have had "don't rely on hash order" drilled into their heads over the decades, and "don't rely on container order" might be a reasonable abstraction step from that, but "don't rely on < behavior" strikes me as unintuitive and going against the intuition users build up from common concrete manifestations of '<', such as numeric types.

-Joe

···

On Mar 9, 2016, at 11:41 AM, Dave Abrahams <dabrahams@apple.com> wrote:

or a desired one.

That's true. But what's important is that it's consistent with == and
allows lookup in a sorted container. The core ordering operation should
not be spelled "<" though; we should use "<=>" for that so that types
can keep their domain-specific "<" if necessary. Too bad the same trick
doesn't work for "==" :-).

Many people have complained that 'nil < .Some(1)' works for optionals,
for instance, ordering 'nil' below Some values. We could use pointer
identity to order class instances and types, but this wouldn't be a
stable ordering across process runs.

Oh, that's what you mean by "stable." So what?


(Dave Abrahams) #14

- Function types in Swift do not provide a ready equality

operation. We could provide a default implementation that always
returns 'false', perhaps.

I think this sort of puts the lie to the idea.

We can always provide *a* definition of equality, but I suspect it
will often be an *incorrect* definition.

I disagree; IMO it would seldom be incorrect.
But I wouldn't necessarily want to make everything equatable. I'd want
to give many things an equatable conformance that's available simply by
declaring it. In fact, I'd like to be able to say:

          struct Something : Regular {
              // Stored properties that are all Regular
          }

and get Equatable, Comparable, and Hashable for free.

That's why you had to suggest functions should always be false: you
cannot (without more effort than you want to spend) provide a correct
definition of equality for it.

I mean, imagine what happens if you make functions Equatable and
Hashable but with definitions that don't actually work. Currently,
`Set<Void -> Void>` gives you an error:

  error: type 'Void -> Void' does not conform to protocol 'Hashable'

But with this feature in place, Swift would happily produce a Set of
functions which collides endlessly, doesn't do any uniquing, never
says it contains any value you pass into it, and can only remove
elements by index.

A type that is "never equal" completely breaks Set in practice, and
there's no way for the type system to catch the problem.

If we automatically synthesize a == operator for every type, many of
those operators will be incorrect. For instance, anything that
includes a cache will be incorrect.
Anything that includes a pointer to a buffer and ought to evaluate the
buffer's contents will be incorrect.
Anything that includes a closure will be incorrect. Individually, each
of these cases is minor, but they multiply and interact with each
other until, together, they undermine confidence in ==.

These things wouldn't be Regular by themselves. To make them
composable, you can create a single Regular value type that wraps each
one.

If you explicitly mark things as Equatable, then it is clear that the
equality operator on them really does have a sensible definition. But
if you can pass anything into ==, you will never know what will
actually *work*. If everything's Equatable, then nothing is.

* * *

Auto-deriving is a different story, though, especially if it's opt-in
(you have to say `deriving Equatable`). There, you presumably have
looked at the default semantics and determined they're appropriate for
your type.

I don't know if that level of explicitness is needed. Explicit
declaration of conformance might be enough.

But I think it's clear that derived conformances should eventually be
a user-accessible feature. We already derive RawRepresentable and
ErrorType on enums. (We also derive initializers on some types, but
that's arguably a separate feature.) I think it's clear that Swift ∞
ought to allow you to derive protocol conformances; it's just a matter
of scheduling.

So I think that what we ought to do is this:

• Make a best guess at what Swift ∞ would want you to do to invoke the
user-specified derivation logic for an arbitrary protocol—implicit
derivation or something marked by a keyword.
• Think about whether derived conformances of Equatable, Hashable,
and/or Comparable are urgent enough that we should implement them
before the general feature.
• If so, design these features along the lines of what we would expect
the eventual user-specified derivation feature to use.

Of course.

···

on Tue Mar 08 2016, Brent Royal-Gordon <brent-AT-architechies.com> wrote:

--
-Dave


(Dave Abrahams) #15

Agreed. There are plenty of systems where == on classes means === by
default, and then people forget to override == when they're making a
class type that doesn't need identity.

(Then again, should such a type be wrapped in a value type in Swift,
to communicate that it doesn't use identity?

Yes.

But then that type will derive ==.)

Yes, but that's actually fine; its == will be a valid equivalence
relation. If you want something else, define it.

···

on Tue Mar 08 2016, Jordan Rose <jordan_rose-AT-apple.com> wrote:

Making them derivable seems totally reasonable. Today just declaring
Equatable or Hashable is enough to do that in the few places where we
do derive conformances; we could either do that or invent a new
"deriving" syntax.

Jordan

On Mar 8, 2016, at 14:15, Austin Zheng via swift-evolution <swift-evolution@swift.org> wrote:

I would prefer Equatable and Hashable to remain opt-in, and for us
to add better support for automatic deriving of implementation.

For something like printing the representation of an object to a
string, there exists a "not wrong" mapping of every possible value
to a string. That is, if my FooStruct doesn't provide a custom
description, having the runtime convert it to something like
"(FooStruct instance)" is still a valid mapping. It might not be
useful, but it's not wrong.

I don't think the same applies for equatability. The universal
default behavior for equating two objects is either correct or
incorrect, and it's not possible to know beforehand which is
which. One of the wonderful things about the current Swift system is
that (modulo some exceptional cases) only things explicitly meant to
be equatable with each other are comparable. We avoid the
object-oriented pitfall in which 'equality' means two different
things - equality of value if you implemented an override properly;
a default 'equality of instance' otherwise (which might be right or
wrong). Of course, the same pitfall wouldn't necessarily apply in
our case, but the problem of having a 'default' == impl that allows
a developer to falsely assume their type is being properly compared
(or not think about it at all) would still be present.

Best,
Austin

On Tue, Mar 8, 2016 at 2:02 PM, Brian Pratt via swift-evolution >> <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> >> wrote:
Definitely a +1 on the basics. When you get inheritance involved, does that complicates things a little bit?

Let's say I have a subclass instance that has corresponding fields
with a superclass instance. Is it equal to said super-class instance
using just member-wise comparisons? Would that be problematic? In
Scala you'd often use a reference to an "equality contract" object
type in order to get "transitive" equality between subclasses and
superclasses, which definitely feels like a step backwards from the
current protocol-driven approach.

On Tue, Mar 8, 2016 at 2:54 PM, Joe Groff via swift-evolution >> <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> >> wrote:
(starting a new thread by DaveA's request)

There's a definition of equality that makes sense as a default for nearly every type in our system:

- Basic types like IntNN, FloatNN, String, etc. have domain-defined equality,
- Structs and tuples can be considered equal if their corresponding fields are equal,
- Enums can be considered equal if they carry the same, equal payload,
- Class references can be considered equal if they refer to the same instance,
- Metatypes can be considered equal if they represent the same type, and
- Existentials can be considered equal if they carry equal values of the same dynamic type.

and similarly, reasonable hash code implementations could be
synthesized by applying a standard hash combine operation over the
components, and a default ordering could be assigned to values of
every type. I think it's worth considering whether Equatable,
Hashable, and/or Comparable, instead of being explicit protocols,
should become universal behavior like 'print', with customization
points to override the default behavior. If Equatable and Hashable
behavior were universal, that would solve many of the common
problems people currently have trying to work with heterogeneous
containers. In object-oriented frameworks, including Cocoa, Java,
and .NET, it is common for the root (NS)Object class to provide
default equality and hashing operations. There are of course some
tradeoffs:

- Universal behavior would require us to either generate code for
'==', 'hashValue', and/or '<' for every type, or provide sufficient
reflection info for a common runtime implementation to do it. The
reflection-based approach may be reasonable for print(), since
dumping reflection info only reduces the quality of the default
logging behavior, but '==' and 'hashValue' are more essential to
proper behavior, so relying on reflection might be too slow, and
would be brittle when we introduce the ability to drop reflection
info.
- Type safety with '==' is important to prevent accidental '1 ==
"1"' type comparsions, and a fully generic 'func ==<T>(x: T, y: T)
-> Bool' could potentially allow those sorts of mixed-type
comparisons by accident. Language rules that constrained when
generic parameters can be resolved to supertypes might help here.
- Function types in Swift do not provide a ready equality
operation. We could provide a default implementation that always
returns 'false', perhaps.
- A Comparable ordering can be dreamt up for many types, but it's
not always a stable ordering, or a desired one. Many people have
complained that 'nil < .Some(1)' works for optionals, for instance,
ordering 'nil' below Some values. We could use pointer identity to
order class instances and types, but this wouldn't be a stable
ordering across process runs. That might be good enough for ordered
collections like search trees, but is weaker than what many people
expect '<' to do.

It's my feeling that Equatable and Hashable would make a lot of
sense as universal operations; I'm not so sure about Comparable.

-Joe
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
-Dave


(Thorsten Seitz) #16

I agree with Austin.
-1 to universal equality

-Thorsten

···

Am 08.03.2016 um 23:15 schrieb Austin Zheng via swift-evolution <swift-evolution@swift.org>:

I would prefer Equatable and Hashable to remain opt-in, and for us to add better support for automatic deriving of implementation.

For something like printing the representation of an object to a string, there exists a "not wrong" mapping of every possible value to a string. That is, if my FooStruct doesn't provide a custom description, having the runtime convert it to something like "(FooStruct instance)" is still a valid mapping. It might not be useful, but it's not wrong.

I don't think the same applies for equatability. The universal default behavior for equating two objects is either correct or incorrect, and it's not possible to know beforehand which is which. One of the wonderful things about the current Swift system is that (modulo some exceptional cases) only things explicitly meant to be equatable with each other are comparable. We avoid the object-oriented pitfall in which 'equality' means two different things - equality of value if you implemented an override properly; a default 'equality of instance' otherwise (which might be right or wrong). Of course, the same pitfall wouldn't necessarily apply in our case, but the problem of having a 'default' == impl that allows a developer to falsely assume their type is being properly compared (or not think about it at all) would still be present.

Best,
Austin

On Tue, Mar 8, 2016 at 2:02 PM, Brian Pratt via swift-evolution <swift-evolution@swift.org> wrote:
Definitely a +1 on the basics. When you get inheritance involved, does that complicates things a little bit?

Let's say I have a subclass instance that has corresponding fields with a superclass instance. Is it equal to said super-class instance using just member-wise comparisons? Would that be problematic? In Scala you'd often use a reference to an "equality contract" object type in order to get "transitive" equality between subclasses and superclasses, which definitely feels like a step backwards from the current protocol-driven approach.

On Tue, Mar 8, 2016 at 2:54 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:
(starting a new thread by DaveA's request)

There's a definition of equality that makes sense as a default for nearly every type in our system:

- Basic types like IntNN, FloatNN, String, etc. have domain-defined equality,
- Structs and tuples can be considered equal if their corresponding fields are equal,
- Enums can be considered equal if they carry the same, equal payload,
- Class references can be considered equal if they refer to the same instance,
- Metatypes can be considered equal if they represent the same type, and
- Existentials can be considered equal if they carry equal values of the same dynamic type.

and similarly, reasonable hash code implementations could be synthesized by applying a standard hash combine operation over the components, and a default ordering could be assigned to values of every type. I think it's worth considering whether Equatable, Hashable, and/or Comparable, instead of being explicit protocols, should become universal behavior like 'print', with customization points to override the default behavior. If Equatable and Hashable behavior were universal, that would solve many of the common problems people currently have trying to work with heterogeneous containers. In object-oriented frameworks, including Cocoa, Java, and .NET, it is common for the root (NS)Object class to provide default equality and hashing operations. There are of course some tradeoffs:

- Universal behavior would require us to either generate code for '==', 'hashValue', and/or '<' for every type, or provide sufficient reflection info for a common runtime implementation to do it. The reflection-based approach may be reasonable for print(), since dumping reflection info only reduces the quality of the default logging behavior, but '==' and 'hashValue' are more essential to proper behavior, so relying on reflection might be too slow, and would be brittle when we introduce the ability to drop reflection info.
- Type safety with '==' is important to prevent accidental '1 == "1"' type comparsions, and a fully generic 'func ==<T>(x: T, y: T) -> Bool' could potentially allow those sorts of mixed-type comparisons by accident. Language rules that constrained when generic parameters can be resolved to supertypes might help here.
- Function types in Swift do not provide a ready equality operation. We could provide a default implementation that always returns 'false', perhaps.
- A Comparable ordering can be dreamt up for many types, but it's not always a stable ordering, or a desired one. Many people have complained that 'nil < .Some(1)' works for optionals, for instance, ordering 'nil' below Some values. We could use pointer identity to order class instances and types, but this wouldn't be a stable ordering across process runs. That might be good enough for ordered collections like search trees, but is weaker than what many people expect '<' to do.

It's my feeling that Equatable and Hashable would make a lot of sense as universal operations; I'm not so sure about Comparable.

-Joe
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Mark Sands) #17

I apologize upfront for necromancing this thread. Is there any momentum on
this proposal?

My team was discussing the merits of default (opt-in) Equality. As soon as
the equality function has been written then the compiler is satisfied, but
there is no way to get compile time correctness for the equality. Unit
tests can only go so far with ensuring the equality function is robust.
Once a new property is added to a struct, for instance, the equality
function is no longer correct and without warning. Currently there is no
way to safeguard this situation. This would be a welcome addition to Swift.

Mark

···

On Fri, Mar 11, 2016 at 11:19 AM, Jose Cheyo Jimenez via swift-evolution < swift-evolution@swift.org> wrote:

> (starting a new thread by DaveA's request)
>
> There's a definition of equality that makes sense as a default for
nearly every type in our system:
>
> - Basic types like IntNN, FloatNN, String, etc. have domain-defined
equality,
> - Structs and tuples can be considered equal if their corresponding
fields are equal,
> - Enums can be considered equal if they carry the same, equal payload,

+1 for Equality, Comparable and Hashable especially for enums and tuples.

I know that equality was added for tuples but I think that enums are
perfect for adding automatic generation of conformance to Equality,
Comparable and Hashable.

Take a look at this toy example of an Enum with 9 cases

boiler plate to conform to Equatable

https://github.com/exercism/xswift/blob/master/exercises/poker/PokerExample.swift#L151-L189

boiler plate to conform to Comparable

https://github.com/exercism/xswift/blob/master/exercises/poker/PokerExample.swift#L191-L245

> It's my feeling that Equatable and Hashable would make a lot of sense as
universal operations; I'm not so sure about Comparable.
>
> -Joe

For enums a defaultComparable protocol could just rank on the order the
fields are declared:

https://github.com/exercism/xswift/blob/master/exercises/poker/PokerExample.swift#L211-L212

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Austin Zheng) #18

As Brent pointed out, adding this sort of support opens a whole can of worms. Large parts of the standard library would silently become unsound.

As well, in my experience people who have had trouble using (e.g.) Equatable with heterogeneous collections are often trying to do type-unsound things. Maybe Swift should support a separate notion of heterogenous equality for comparisons between Equatable types (and one of the POP WWDC talks actually sketched out an outline of how this might be done), but that's different from making Equatable universal. In addition, I think Swift 3's proposed support for conditional protocol conformance will make creating principled heterogeneous collections easier, which should ease some of the burden.

Best,
Austin

···

On Mar 9, 2016, at 12:17 AM, David Hart <david@hartbit.com> wrote:

On 08 Mar 2016, at 23:15, Austin Zheng via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I would prefer Equatable and Hashable to remain opt-in, and for us to add better support for automatic deriving of implementation.

On 08 Mar 2016, at 23:57, Zach Waldowski via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I completely agree with Austin here. Automatic derivation (perhaps through the same mechanisms Joe is talking about) would be a nice enhancement, but I find it refreshing and advantageous for simple value types to have very little automatic behavior.

Pedantically I agree with both of you, but from a very pragmatic point of you, I think it's very important to point out what Joe said about how this could reduce one of the most frustrating aspects of Swift, when people work with heterogeneous arrays and try to conform to Equatable:

that would solve many of the common problems people currently have trying to work with heterogeneous containers.


(Haravikk) #19

While I appreciate the idea behind the proposal, I think I’m a -1 to it. Java has required equality and hashable as part of its base Object class, but I frequently encountered classes that had very poor implementations for these, or never bothered to provide one; arguably they didn’t need to, which is fine, but it kind of went against the whole idea.

Swift has some pretty nifty features that also make this redundant, for example, I’ve been working on some ordered collection types; my natural inclination was to require that values be Comparable, however this actually limits the usefulness of the collection (or requires values to be wrapped somehow). Instead I decided to accept values of any type, and also take a closure (same as used to sort an array).

However, with generic constraints I can still provide a default closure for Comparable types like so:

// Sort Comparable elements in ascending order if no closure is provided.
extension OrderedCollection where Self.Generator.Element:Comparable {
  init<S:SequenceType where S.Generator.Element == Generator.Element>(elements:S) {
    self.init(isOrderedBefore: { $0 < $1 }, elements: elements)
  }
}

(the same feature also lets me implement ArrayLiteralConvertible for Comparable arrays, though I have to provide a default initialiser producing a fatal error for the rest)

It’s a bit of a weird thing to get your head around at first, but you can solve a lot of problems in a similar way, without having to place overly strict requirements on the types that you can accept, removing the need for all types to conform to anything.

···

On 9 Mar 2016, at 08:30, Austin Zheng via swift-evolution <swift-evolution@swift.org> wrote:

As Brent pointed out, adding this sort of support opens a whole can of worms. Large parts of the standard library would silently become unsound.

As well, in my experience people who have had trouble using (e.g.) Equatable with heterogeneous collections are often trying to do type-unsound things. Maybe Swift should support a separate notion of heterogenous equality for comparisons between Equatable types (and one of the POP WWDC talks actually sketched out an outline of how this might be done), but that's different from making Equatable universal. In addition, I think Swift 3's proposed support for conditional protocol conformance will make creating principled heterogeneous collections easier, which should ease some of the burden.

Best,
Austin

On Mar 9, 2016, at 12:17 AM, David Hart <david@hartbit.com <mailto:david@hartbit.com>> wrote:

On 08 Mar 2016, at 23:15, Austin Zheng via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I would prefer Equatable and Hashable to remain opt-in, and for us to add better support for automatic deriving of implementation.

On 08 Mar 2016, at 23:57, Zach Waldowski via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I completely agree with Austin here. Automatic derivation (perhaps through the same mechanisms Joe is talking about) would be a nice enhancement, but I find it refreshing and advantageous for simple value types to have very little automatic behavior.

Pedantically I agree with both of you, but from a very pragmatic point of you, I think it's very important to point out what Joe said about how this could reduce one of the most frustrating aspects of Swift, when people work with heterogeneous arrays and try to conform to Equatable:

that would solve many of the common problems people currently have trying to work with heterogeneous containers.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Sean Heber) #20

We can also allow opt-in by adopting a protocol, eg `struct SimpleStruct: DefaultEquatable`. DefaultEquatable would tell the compiler to auto-generate an equatable implementation. Similar for DefaultHashable.

In the C# world you’d actually do something like this with an attribute. I know user-defined attributes aren’t on the table right now but some more built-in ones would be sufficient: @defaultEquatable, @defaultHashable, etc.
They could even take a list of properties to ignore if such a feature were useful: `@defaultEquatable(ignored: [currentDate, randomInt])`.

Might be interesting if you could pass in parameters when you declare conformance, like:

struct MyType : Equatable(default) {}

If “default” was missing, you’d have to supply the relevant ==/equals function or whatever as usual, but if it was there you’d get automatically generated stuff based on documented rules.

Maybe it could even be extensible so you can define protocol extensions that are “named” and only apply if you declare conformance with the relevant name:

protocol Bar {
  func barFunction()
}

extension Bar(foo) {
  func fooFunction() { }
}

struct Type1 : Bar(foo) {
  func barFunction() {}
}

struct Type2 : Bar {
  func barFunction() {}
}

A Type1 instance would also have a fooFunction(), but a Type2 instance would not.

Using this you could have multiple default implementations defined for the same protocol depending on circumstances.

extension Bar(scenerio1) {
  func barFunction() { print(“1”) }
}

extension Bar(scenerio2) {
  func barFunction() { print(“2") }
}

struct Thing1 : Bar(scenerio1) {}
struct Thing2 : Bar(scenerio2) {}

let a: Bar = Thing1()
let b: Bar = Thing2()

a.barFunction() -> “1”
b.barFunction() -> “2”

:slight_smile:

l8r
Sean