Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Champion "Support for == and != on tuple types" (15.7) #190

Open
2 of 5 tasks
gafter opened this issue Feb 26, 2017 · 72 comments
Open
2 of 5 tasks

Champion "Support for == and != on tuple types" (15.7) #190

gafter opened this issue Feb 26, 2017 · 72 comments
Assignees
Labels
Implemented Needs ECMA Spec This feature has been implemented in C#, but still needs to be merged into the ECMA specification Proposal champion
Milestone

Comments

@gafter
Copy link
Member

gafter commented Feb 26, 2017

Summary

Support == and != on tuples.
For example (x, y) == (1, 2) (which would be equivalent to x == 1 && y == 2).

See also dotnet/roslyn#13155

LDM history:

Started prototype at tuple-equality

@gafter gafter added this to the 7.X candidate milestone Feb 26, 2017
@Thaina
Copy link

Thaina commented Feb 27, 2017

Should just support extension operator on IEquatable<T>

@gafter
Copy link
Member Author

gafter commented Feb 27, 2017

@Thaina No, == and .Equals are different things. Try double.NaN for example. Also, the == operator would convert the values in a tuple, elementwise, to a common type for comparison purposes, while .Equals requires that they be the same type.

@DavidArno
Copy link

Retracting my up-vote for this proposal. As I read "No, == and .Equals are different things", I do not just witness the principle of least astonishment being violated, it's being smashed into tiny pieces and ground into the dirt.

Yes, for historical reasons, we have ended up with multiple ways of testing for equality and this is one of those "we are stuck with it" things that can't be undone. But perpetuating this messy situation by having new types like ValueTuple<...> implement different behaviour for == and .Equals is pure madness IMO.

We would be better off with the team just leaving this topic well alone and implementing the "extension everything" proposal instead. Then 3rd party libraries can implement == and .Equals as the same thing and the great disturbance in the force will fade away...

@ig-sinicyn
Copy link

@DavidArno

We would be better off with the team just leaving this topic well alone and implementing the "extension everything" proposal instead. Then 3rd party libraries can implement == and .Equals as the same thing and the great disturbance in the force will fade away...

so, double.Nan == double.Nan should be false but
(double.Nan, double.Nan) == (double.Nan, double.Nan)
will be true, false, NaN or a tiny pink elephant depending on a developer's preferences? Pure evil 👍

@HaloFour
Copy link
Contributor

Tuples being just a loosely bound sequence of values I think that it makes more sense to be able to compare those values irrespective of the container.

(int, int) tuple1 = ...;
(int, int) tuple2 = ...;

if (tuple1 == tuple2) { ... }
// the same as
if ((tuple1.Item1 == tuple2.Item1 && tuple1.Item2 == tuple2.Item2)) { ... }

@DavidArno
Copy link

DavidArno commented Feb 28, 2017

@ig-sinicyn,

In the following code, result should be true:

var x = double.Nan;
var y = double.Nan;
var result = (x == y) == x.Equals(y);

But it's false as double implements == and .Equals differently.

You appear to be proposing that, just because the CLR and language teams couldn't agree on whether NaN is the same as NaN, then tuples equality should perpetuate this nonsense. Having (double.Nan, double.Nan) == (double.Nan, double.Nan) would be a tiny price to pay for equality sanity in tuples.

@DavidArno
Copy link

DavidArno commented Feb 28, 2017

@HaloFour,

struct S { }

(S, S) tuple1 = ...;
(S, S) tuple2 = ...;

if (tuple1.Item1 == tuple2.Item1 && 
    tuple1.Item2 == tuple2.Item2) // oh dear: compiler error

ValueTuple<T1, T2> cannot implement == as the above as it's not constrained to T1 and T2 being classes.

@HaloFour
Copy link
Contributor

@DavidArno

Yes, and I have no problem with that. I don't think that the fact that the two values are contained within a tuple should matter.

@orthoxerox
Copy link

@DavidArno Did you know that comparison operators and CompareTo produce different results on doubles as well? That's not something that .Net invented either. Comparison and total order operations are defined differently on doubles by IEEE. Equals matches the behavior of CompareTo.

@DavidArno
Copy link

@orthoxerox, @HaloFour, @ig-sinicyn,

So you have all done a lot of "no, no, can't be .Equals() and @gafter has ruled out the IEquatable<T>/EqualityComparer route and == on T1 etc can't be used. So what solution is left?

@HaloFour
Copy link
Contributor

@DavidArno

The solution is that the developer has to use Equals manually rather than relying on == or !=, which is the same answer as when tuples aren't involved.

@orthoxerox
Copy link

orthoxerox commented Feb 28, 2017

@DavidArno the compiler has to rewrite the left == right expression as (left.Item1 == right.Item1 && left.Item2 == right.Item2). If one of the types is a struct without ==, it will be a compiler error.

@DavidArno
Copy link

DavidArno commented Feb 28, 2017

@orthoxerox,

The following would have to be a compiler error therefore, as == in tuples would be a compiler trick, rather than a static operator on the type and so it cannot determine the type at compile-time:

bool F<T1,T2>((T1, T2) x, (T1, T2) y) => x == y;

@orthoxerox
Copy link

@DavidArno it's an error now and I don't see how it is related to equality of tuples.

@DavidArno
Copy link

@orthoxerox,

Apologies: the code was wrong. I've updated it to what I meant to say.

@HaloFour
Copy link
Contributor

@DavidArno

So, how about the compiler first attempts to resolve == operators defined on ValueTuple<...> via extensions and if one isn't found it instead falls back to evaluating the elements individually?

I don't know that I like that, though. If you needed the container of those two values to matter, enough to define their equality, then you probably shouldn't be using tuples and you should define a proper type instead.

@orthoxerox
Copy link

@DavidArno Yes, it looks like it will have to be a compiler error, just like this function is right now:

bool F<T1,T2>(T1 x1, T2 x2, T1 y1, T2 y2) => x1 == y1 && x2 == y2;

P.S. I've just spent an hour enumerating all the equality mechanisms of the CLR and I've yet to cover the interfaces.

@CyrusNajmabadi
Copy link
Member

You appear to be proposing that, just because the CLR and language teams couldn't agree on whether NaN is the same as NaN

That's not what's happening.

== implement IEEE754 semantics. .Equals impelments .Net equals semantics. IEEE requires that == not be reflexive for NaN. However, .Equals is required to be reflexive. i.e. object.Equals states that "a.Equals(a)" must be true for all instances.

If we broke IEEE semantics that would be a problem for tons of customers. If we broke .net semantics that would be a problem for tons of .net use cases (for example, you could not use a double effectively as a key inside a hashtable). This approach gives both sets of customers the semantics they require.

@CyrusNajmabadi
Copy link
Member

ValueTuple<...> implement different behaviour for == and .Equals is pure madness IMO.

No, it really isn't. For example, today, if i have:

byte b = 1;
int i = 1;

Console.WriteLine(b == i);
Console.WriteLine(b.Equals(i));

I will print "True, False".

== and .Equals are already very different.

The problem compounds with tuples. Tuples just aggregate data. If you aggregate data that uses to compare the same with == but now compares differently (or vice versa), then tuples no longer act properly a composition concept.

@CyrusNajmabadi
Copy link
Member

Using hte above example, if we do not support == on tuples then you can get the following badness:

byte b;
string s;

(byte, string) t1 = (b, s);
(int, string) t2 = (b, s);

Console.WriteLine(t1.Item1 == t2.Item1 && t2.Item2 == t2.Item2);
Console.WriteLine(t1 == t2); // doesn't currently compile.  But we would like it to.  Can't be implemented through .Equals
Console.WriteLine(t1.Equals(t2));

The top and bottom lines will print 'true', and 'false'. I strongly believe the second line shoudl print 'true' and that it should just be exactly transformed as:

t1 == t2 
// becomes
t1.Item1 == t2.Item1 && ... && t1.ItemN == t2.ItemN

This, of course, then means that nested tuples work properly (something that definitely does not work with .Equals).

@DavidArno
Copy link

Ha, ha, thanks @CyrusNajmabadi, the idea of someone trying to use a double as a key in a hashtable is going to keep me chuckling all day! 😁 Even JavaScript, where it'll let black equal white every other Thursday except on leap years doesn't treat NaN as equal. The idea that the .NET team's dogma was so strong that they'd break an international standard just to enable doubles to be used as Dictionary keys is mind-boggling.

Anyway, that aside, you didn't address the important point from above. If a value tuples' types are structs, they may not implement == themselves. So how would you propose this feature work?

@CyrusNajmabadi
Copy link
Member

the idea of someone trying to use a double as a key in a hashtable is going to keep me chuckling all day!

Using a double as a key is completely fine to do.

The idea that the .NET team's dogma was so strong that they'd break an international standard just to enable doubles to be used as Dictionary keys is mind-boggling.

It's interesting that you complain about things being inconsistent, but then complain when .net insisted that .Equals behave consistently :)

Anyway, that aside, you didn't address the important point from above. If a value tuples' types are structs, they may not implement == themselves. So how would you propose this feature work?

The exact way that == works for types that don't have an available == operator. I'm stating that == for tuples is defined as:

(x1, ..., xn) == (y1, ..., yn)   <==>  x1 == y1 && ... && xn == yn

== on the tuple will work in precisely all the circumstances that == works for the constituent elements.

just to enable doubles to be used as Dictionary keys is mind-boggling.

I never said that. Please do not put words in my mouth. I simply gave an example of how the .Equals behavior is desirable and consistent. I did not say that it was done "just" for this reason.

--

Also, to be clear, you never addressed the other points i made. For example, that == and .Equals are not equivalent today for ordinary numeric types other than double. Today == will return true when .Equals does not. == follows the rules of C# while .Equals follows the rules of .Net. Making it so that we have a type in C# which composes other C# types, but which does not compose == appropriately just leads to the composition not actually happening fully.

@CyrusNajmabadi
Copy link
Member

Note: having .Equals be symmetric is nice for lots of reasons. After all, while it would be very weird to have "a.Equals(a)" be false, it would be even more confusing and difficult to use .net effectively if doubles didn't support this concept. After all, the following could then be false:

list.Add(d);
Console.WriteLine(list.Contains(d))

Effectively you would make it so that doubles could not actually work effectively as any sort of object in any sort of container. You could not use them effectively in linq. They would always be something that never meshed well with the rest of .Net. So .Net implements .Net semantics. If you want IEEE semantics, you can use the simple operators.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Mar 1, 2017

I'd like the table the Double discussion as the interesting and esoteric behaviors of Double are not what are important here. All the reasons for tuples needing == support can be explained with simpler types like int/long.

For people who don't think == should be lifted to tuples, can i ask why you think the following should behave differently:

int i = 0;
long j = 0;

Console.WriteLine(i == j); // what do you think this should print?

(int, int) t1 = (i, i);
(long, long) t2 == (j, j);
Console.WriteLine(t1 == t2); // what do you think this shoudl print?

If you think the answer should be 'true' then 'false', can you explain why you think that two values which previous were == true, should now not compare the same just because they have been packaged up into a pair of values?

@DavidArno
Copy link

DavidArno commented Mar 1, 2017

@CyrusNajmabadi

int i = 0;
long j = 0;

Console.WriteLine(i == j); // what do you think this should print?

I think it should print exactly the same as for:

int i = 0;
long j = 0;

Console.WriteLine(i.Equals(j)); 

Whether it's true or false isn't important - that's just convention.

As Eric Lippert says in his Sharp Regrets: Top 10 Worst C# Features article, equality in C# is one of its top ten worst features. But what is done is done and we must live with it. However, watching you try to defend it, rather than just admitting it was a mistake that we can all learn from, is more than a little frustrating.

I'm still confused as to how you think == will work in tuples though, based on your expression:

(x1, ..., xn) == (y1, ..., yn)   <==>  x1 == y1 && ... && xn == yn

As per my previous example, struct S {}, how will the following work?

var x = (new S(), new S());
var y = (new S(), new S());
if (x == y) ...

@Thaina
Copy link

Thaina commented Mar 1, 2017

@DavidArno What you proposed might be ideal but it could be breaking change so that's a problem. Sometimes we need to deal with legacy

@CyrusNajmabadi
Copy link
Member

I think it should print exactly the same as for:

Ok. Given that that's not how C# has behaved since v1, that seems bad. You are complaining about inconsistency, but would not like C# vNext to be inconsistent with C# v1-v7.

As per my previous example, struct S {}, how will the following work?

var x = (new S(), new S());
var y = (new S(), new S());
if (x == y) ...

it's exactly as i stated, it would be equivalent to:

if (x.Item1 == y.Item1 && x.Item2 == y.Item2)

If S has no == operator then i would expect an error. precisely as i would if i wrote:

var v1 = new S();
var v2 = new S();
Console.WriteLine(v1 == v2);

@DavidArno
Copy link

DavidArno commented Mar 8, 2017

@jcouv,

I have (well had, see below) a single motivation. I don't like the fact that value tuples are mutable. The natural conclusion (in my head, anyway) was therefore to create my own - immutable - version.

However, I've reconsidered that plan, based on two things:

  1. The compiler will, from 7.1 onwards (or is it in 7?) make assumptions about the behaviour of those tuples,
  2. I read today (from a Roslyn issue you were involved in, I think, but I can't find it now), that from eg .NET framework v4.7, the ValueTuple and new async feature nuget packages will be baked into the framework.

Therefore, I no longer consider it wise to create a custom ValueTuple implementation.

@HaloFour
Copy link
Contributor

HaloFour commented Mar 8, 2017

@alrz

I wonder if this could be implemented on top of shapes rather than hard-coding into the compiler. Not only equality, almost all of tuple operations should be distributed over all elements (if all of them satisfy a certain set of constraints which is representable through generic shapes).

e.g. using over a tuple full of IDisposables or await over a tuple full of elements for which the compiler can resolve GetAwaiter()?

In those cases it might make sense, but I think it might not always be so obvious how an operation should be distributed, if it should be at all. For the most appropriate behavior it might be necessary for the compiler to special-case the operations in question.

@alrz
Copy link
Member

alrz commented Mar 8, 2017

I think it might not always be so obvious how an operation should be distributed, if it should be at all. For the most appropriate behavior it might be necessary for the compiler to special-case the operations in question.

That directly depends on the shape implementation. I'm not sure why "special-casing" would make that more "appropriate". I think this would be the most sensible starting point for designing shapes. Once it's possible to model various operations through shapes there is no reason to special-case such features. IMO it'd be worth it to invest on the general solution rather than special-casing all these into the compiler.

@Richiban
Copy link

Richiban commented Jun 5, 2017

@alrz

Once it's possible to model various operations through shapes there is no reason to special-case such features

I would normally agree with you, except I do think that it might be appropriate in the case of tuples. Because they should really be such a basic and integral feature I'm all in favour of the compiler 'inlining' the body of the '==' method for tuples, even if the method exists (at least notionally) as the operator '==' on a ValueTuple where T1, T2 etc are members of an Eq typeclass.

@alrz
Copy link
Member

alrz commented Jun 5, 2017

The case for inlininig was discussed at length in other threads before and concluded that roslyn is not the place for these optimizations (even though there were some good arguments against it, for instance, dotnet/roslyn#15644). In fact, that was my first concern regarding how shapes are implemented (#164) but as others have said, invocations of shape methods get specialized by the JIT.

@alrz
Copy link
Member

alrz commented Feb 20, 2018

How is this feature different from tuple patterns?

(1, 2, 3) is (1, 2, 3)
(1, 2, 3) == (1, 2, 3)

I think it's not, other than that you could compare variables with ==, right?

@jcouv
Copy link
Member

jcouv commented Feb 20, 2018

@alrz This feature allows comparing two tuple variables (tuple1 == tuple2) as well, whereas positional patterns require constants.

@DavidArno
Copy link

@jcouv,

That doesn't sound right to me. Surely positional patterns for tuples will support recursive patterns, not just constants?

tuple is (var x, (1, var y),  3)

@alrz,
Surely "I think it's not, other than that you could compare variables with ==, right?" applies to all basic patterns, doesn't it?

x is 1
x == 1

@HaloFour
Copy link
Contributor

@DavidArno

Yes, but the only actual values that you can compare must be themselves constants. So if you're comparing to other variables via pattern matching you'd have to use pattern variables and guards:

public void Foo((int, int) x, (int, int) y) {
    switch (x) {
        case (var a, var b) when a == y.Item1 && b == y.Item2: ...
    }
    // vs
    if (x == y) { ... }
}

I assume that you could also use it as shorthand for comparing equality of multiple items:

public void Foo(int a, int b, int c, int d, int e, int f) {
    if (a == d && b == e && c == f) { ... }
    // vs
    if ((a, b, c) == (d, e, f)) { ... }
}

@DavidArno
Copy link

DavidArno commented Feb 21, 2018

@HaloFour

I absolutely love the idea of:

public void Foo(int a, int b, int c, int d, int e, int f) {
    if ((a, b, c) == (d, e, f)) { ... }
}

Hopefully, @jcouv can work the same magic as he did for the immodestly named Arno Assignment Pattern so that the tuples get optimised away here too.

@jcouv
Copy link
Member

jcouv commented Feb 21, 2018

@DavidArno (a, b, c) == (d, e, f) will likely require referencing the ValueTuple types for the compilation to succeed, but no ValueTuple reference will be emitted in IL.

@jnm2
Copy link
Contributor

jnm2 commented Feb 21, 2018

@jcouv Couldn't the ValueTuple requirement be fixed the same way it was for deconstruction?

@jcouv
Copy link
Member

jcouv commented Feb 22, 2018

@jnm2 It's not obvious. Still looking into it ;-)

@jcouv jcouv changed the title Champion "Support for == and != on tuple types" Champion "Support for == and != on tuple types" (15.7) Mar 16, 2018
@gafter gafter added this to C# 7.3 in Language Version Planning Mar 6, 2019
@333fred 333fred added the Implemented Needs ECMA Spec This feature has been implemented in C#, but still needs to be merged into the ECMA specification label Oct 16, 2020
@333fred 333fred removed this from C# 7.3 in Language Version Planning Feb 6, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Implemented Needs ECMA Spec This feature has been implemented in C#, but still needs to be merged into the ECMA specification Proposal champion
Projects
None yet
Development

No branches or pull requests

15 participants