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

Add note on collection equality on 'Collections' page #5629

Open
1 task
dtonhofer opened this issue Mar 6, 2024 · 3 comments
Open
1 task

Add note on collection equality on 'Collections' page #5629

dtonhofer opened this issue Mar 6, 2024 · 3 comments
Labels
from.page-issue Reported in a reader-filed concern

Comments

@dtonhofer
Copy link

dtonhofer commented Mar 6, 2024

Page URL

https://dart.dev/language/collections/

Page source

https://github.com/dart-lang/site-www/tree/main/./src/content/language/collections.md

Describe the problem

On page collections one should highlight the fact that "equality of a collection" falls back to the default "object identity", i.e. using == on collections just checks whether the objects on the left and right of == are "identical".

The user might expect that == compares the collection contents, which would be:

  • lists: perform an equality check (which may or may reduce to an identity check) on corresponding elements (corresponding by index, that is) from both lists.
  • maps: perform an equality check (which may or may reduce to an identity check) on values associated to keys that are equal (which may or may not mean that they are identical) in both lists.
  • sets: check that each elements has an equal element (possibly the identical elemnt) in the other set

One might redirect the user to consult libraries such as quiver which provide appropriate facilities.

For fun, some example code I just wrote:

import 'package:quiver/collection.dart';

// Class Foo does not override its "==" operator.
// This two Foo objects are "equal" according to "=="
// if and only if they are "the same object".

class Foo {
  final int x;

  Foo(this.x);
}

// Class Bar override its "==" operator so two Bar objects
// are "equal" according to "==" if they "are the same value", in
// this case, if the value of their field "x" is the same.
// This is called "having value semantics".

class Bar {
  final int x;

  Bar(this.x);

  @override
  bool operator ==(Object other) => (other is Bar && x == other.x);

  @override
  int get hashCode => x;
}

void testMapIdentityAndEquality() {
  // Integers are "equal" if they "are the same value" ("value semantics")

  final mapInt1 = {'x': 1, 'y': 2, 'z': 3};
  final mapInt2 = {'x': 1, 'y': 2, 'z': 3};

  // Foo objects are "equal" only if they "are the same object".

  final mapFoo1 = {'x': Foo(1), 'y': Foo(2), 'z': Foo(3)};
  final mapFoo2 = {'x': Foo(1), 'y': Foo(2), 'z': Foo(3)};

  // Bar objects are "equal" if they "are the same value" ("value semantics")

  final mapBar1 = {'x': Bar(1), 'y': Bar(2), 'z': Bar(3)};
  final mapBar2 = {'x': Bar(1), 'y': Bar(2), 'z': Bar(3)};

  // Identity and equality of maps holding elements with value semantics.

  assert(identical(mapInt1, mapInt1), "a map is (evidently) IDENTICAL to itself");
  assert(mapInt1 == mapInt1, "a map that is the same object is also EQUAL to itself ('==' defaults to object identity)");

  assert(!identical(mapInt1, mapInt2), "a map is NOT IDENTICAL to another map");
  assert(mapInt1 != mapInt2, "a map is NOT EQUAL to another map, even if the map content is 'the same' and has value semantics");
  assert(mapsEqual(mapInt1, mapInt2), "QUIVER EQUALITY says the maps are EQUAL if map content is 'the same' and has value semantics");

  assert(!identical(mapFoo1, mapFoo2), "a map is NOT IDENTICAL to another map");
  assert(mapFoo1 != mapFoo2, "a map is NOT EQUAL to another map, especially if the map content does not have value semantics");
  assert(!mapsEqual(mapFoo1, mapFoo2), "QUIVER EQUALITY says the maps are NOT EQUAL if the map content 'looks the same' but does not have value semantics");

  assert(!identical(mapBar1, mapBar2), "a map is NOT IDENTICAL to another map");
  assert(mapBar1 != mapBar2, "a map is NOT EQUAL to another map, even if the map content is 'the same' and has value semantics");
  assert(mapsEqual(mapBar1, mapBar2), "QUIVER EQUALITY says the maps are EQUAL if map content is 'the same' and has value semantics"); // what we want

  // More cases

  assert(!mapsEqual(
      <dynamic, dynamic>{'x': Bar(1), 'y': Foo(1)},
      <dynamic, dynamic>{'x': Bar(1),'y': Foo(1)}),
  "QUIVER EQUALITY says the maps are NOT EQUAL if the map content 'looks the same' but at least some of the elements do not have value semantics");
  assert(!mapsEqual(mapBar1, mapFoo1), "Not even the same type");
}

void main() {
  testMapIdentityAndEquality();
}

Expected fix

Update the page.

Additional context

No response

I would like to fix this problem.

  • I will try and fix this problem on dart.dev.
@dtonhofer dtonhofer added the from.page-issue Reported in a reader-filed concern label Mar 6, 2024
@dtonhofer

This comment was marked as resolved.

@dtonhofer dtonhofer changed the title [PAGE ISSUE]: 'Collections' [PAGE ISSUE]: 'Collections' - Mabye a note on "collection equality", i.e. behaviour of == in relation to collections Mar 6, 2024
@atsansone atsansone changed the title [PAGE ISSUE]: 'Collections' - Mabye a note on "collection equality", i.e. behaviour of == in relation to collections Add note on collection equality on 'Collections' page Mar 21, 2024
@atsansone
Copy link
Contributor

@eernstg @munificent : Would you have any insight on this contributor's issue?

@eernstg
Copy link
Member

eernstg commented Mar 22, 2024

First, I do agree with @dtonhofer that this topic merits an explanation.

The way I usually respond when it is argued that collection equality should be based on the contents is that (1) we don't have a distinction between mutable and immutable collections at the level of types, and (2) mutable collections should have an equality which is based on object identity.

Because of (1), it isn't practical to make a distinction between mutable and immutable collections when we're considering an expression that uses == of an object whose static type is a collection type. So we must treat all collections in the same way.

Item (2) is based on the consideration that it is misleading to report that two mutable collections are equal if they aren't the same object: They may contain equal elements right now, but they could differ at any point in the future if at least one of them is mutated. So we may give the developer the strict notion of equality which will actually remain stable over time, plus a notion of equality which is based on the current contents when that's appropriate. Given that object identity is the safe kind of equality it gets the operator ==; the other one gets a name like listEquals such that it must be chosen explicitly when needed.

You could argue that this choice is limiting because there is no way to specify that any given equality comparison should use one or the other (and there could be more than two) in a composite setting. For instance, we might want to compare two objects of type List<Map<Object, Object>>, but we can't easily configure this comparison to use value based equality for the List as well as the Maps, or value based for the list and identity based for the Maps, etc. (OK, the latter variant is actually exactly what listEquals will do.) The same question could be raised dynamically and recursively for collections encountered as keys or values of those Maps. In short, we could wish for more expressive power in this area.

However, I think using identity based equality for collections and offering listEquals and such on the side is a meaningful trade-off.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
from.page-issue Reported in a reader-filed concern
Projects
None yet
Development

No branches or pull requests

3 participants