-
Notifications
You must be signed in to change notification settings - Fork 213
Optional auto-generation of operator== and hashCode on classes with const constructors #1121
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
Comments
Something like https://github.com/a14n/zengen#value :) |
Thanks, @a14n, for the link to https://pub.dartlang.org/packages/zengen |
Quite close, but the generated code isn't exactly what we'd want. We'd want @DoTheThing()
class A {
final a;
final int b;
} to generate class A {
final a;
final int b;
const A({this.a, this.b});
@override bool operator ==(o) => identical(this, o) ||
o.runtimeType == runtimeType && o.a == a && o.b == b;
@override int get hashCode => hashObjects(a, b);
} Also, we don't want to add a source-to-source translation step to our toolchain. We'd like something that the compiler and analyzer understand. |
@abarth I think the proposed desugaring can only work cleanly under the following conditions:
Otherwise, if |
Is there a better @override
bool operator ==(dynamic other) {
assert(debugAssertIsValid());
if (identical(this, other))
return true;
if (other is! BoxConstraints)
return false;
final BoxConstraints typedOther = other;
assert(typedOther.debugAssertIsValid());
return minWidth == typedOther.minWidth &&
maxWidth == typedOther.maxWidth &&
minHeight == typedOther.minHeight &&
maxHeight == typedOther.maxHeight;
} We'd be willing to lose the |
@yjbanov A |
Incidentally, in Flutter's codebase we sometimes check for type equality using The reason you want to use |
I see that as a clear sign that If we had non-virtual methods, I would make (Another approach is to not have your instances extend each other, but have all of them extend a common abstract superclass, with equality only being defined at the leaves of the class tree, then you can use |
We use |
I suspect there's no one implementation of
What is the difference between a source-to-source translator and a compiler? It seems they both need to do the same amount of work and generate the same thing.
Types don't matter in my case. Both classes express the same thing. The choice between one type or the other is purely an implementation detail (perhaps they have different performance characteristics). If |
Maybe a more correct thing to say is that we want something that's fully integrated into the Dart toolchain and has zero impact on the development cycle. The existing source-to-source transformations don't have these properties but macro expansion in C, for example, does have these properties. |
If you're ok with adding a This bug is only about how to provide an |
👍. C's macro expansion happens at source text level and it can blow away the cache, hurting the dev cycle, but I get the point.
Understood. I have nothing against Flutter providing a boilerplate-free way of expressing this, but I would be concerned if the suggested implementation of the |
Oh certainly. As I noted above, even in Flutter's codebase we don't always want the same version. For example equivalent Size and _DebugSize objects want to be considered ==, whereas equivalent Offset and Size objects do not (these all directly or indirectly inherit from OffsetBase). All we're looking for here is a shorthand for a common case we see a lot. It may be that the common case here isn't common enough to justify dedicated syntax and instead there should be macro-like syntax. Or it could be that the common case here really is very common and it can be blessed, while still allowing less common cases to be implemented on a case-by-case basis. |
I vaguely remember seeing a macro-like proposal that would rely on annotations to generate code. IIRC, the use-case was to generate JSON serializers/deserializers from fields and some sort of AOP use-case. Can't find it anymore. I think it was @sigmundch's doc. |
Bingo! Go, @a14n! |
for what it's worth -- C# 7 is going to have this. |
@jmesserly Makes sense as (if I'm reading correctly) they do this only for sealed classes and structs, both of which satisfy the conditions above |
Yup, exactly. From the looks of it they designed record types to make immutable plain-old-data classes very easy & they also thought about how it integrates with pattern matching (https://github.com/dotnet/roslyn/blob/features/records/docs/features/patterns.md) |
FYI Here is a source_gen based approach to this problem: https://github.com/google/built_value.dart source_gen satisfies the requirement to be visible to the analyzer. built_value is modeled after Guava's @autovalue. Re: the equality operator, we follow this from @autovalue: https://github.com/google/auto/blob/master/value/userguide/howto.md#inherit -- and point to Effective Java, which explains that there is no good way to mix equality and inheritance. |
Can you explain this a bit more? |
source_gen uses the build package: https://github.com/dart-lang/source_gen The idea behind "build" is that when developing you run it as a watcher than continuously updates the generated source. So from the point of view of your IDE and the analyzer there's no difference between generated and non-generated source. It's a nice experience, you can navigate easily into the generated code and see what's going on. |
Thanks. Unfortunately, that doesn't meet my requirement to have zero impact on the development cycle. It sounds like a good approach to have a small impact, but it's still nonzero. |
Yes, it's not quite as neat as a language feature. I've given considerable thought to the possibilities around a language feature. Right now I think the source_gen approach is better. If we add something as a language feature, it becomes very fixed/rigid, we can't iterate on it. So my intention is to build with source_gen in the hope of settling on something widely successful/popular. Then there will be a case for upgrading it to a language feature. What I want is more than is described on this request, though. For an immutable class I like to also have a builder class, so you have a mutable version of the type that you can accumulate data in. This is good for readability and efficiency. This is what built_value, linked above, provides. I don't see a nice way to do this with a language feature. |
The source_gen approach is not better for us because it does not meet our requirements. |
Thanks. I understand that source_gen does not meet your requirements. What I meant by "better" is in the short term: the source_gen approach is a realistic way to create interest/support for something before it becomes a language feature. Put another way, if we first have a popular source_gen solution to the problem, there is a much stronger case for adding it as a language feature. I would also like to see something end up in the language at some point, but I'd be surprised if it happens soon, so that's why I'm working on the source_gen path. |
You're welcome to head down the source_gen approach, but it's somewhat off-topic for this bug. |
|
Yes, equality (or, to be exact, "equivalence") is not a property of the object. but of the perspective. In Object Oriented programming, objects often correspond to "real" concepts and entities. That's why you can have a Cartesian point (x=2, y=2) and a polar point (r= √8, φ= π/4) which both represent the same underlying entity - the element (2, 2) of ℝ². On the other hand, if the representation is important to you, you can have a function that expects either a And even further, looking at the actual objects in the system, even two So, why do we have equals on some objects anyway. For one thing, even if there are multiple perspectives on an object, in some cases one perspective is so much more common than any other that it makes sense to give it precedence. Numerical equality of numbers. Contents of strings. It's simply too annoying not to be able to compare two numbers by just writing When you have an interface, it makes sense to declare a notion of equality on that interface, based only on the visible properties. That makes it possible for multiple implementations to agree on the equality. If you implement the interface by extending a base-class, you can even share the equality implementation. The You don't have to give a class a custom equality. Many classes work well with just the default identity - objects are equal only if they are the same objects (that's really the minimal equivalence relation: it is the minimal reflexive relation and that happens to also be trivially symmetric and transitive). So, back to equality on classes that can be extended. Using In summary: Don't put equals on mutable objects or objects likely to be extended. Only put it at the leaves of your class hierarchy, or pick one equality and use it for the entire subtree. And even then it will sometimes suck. |
Re: Comparator, I think the answer is the same: you can do this right now with codegen, so it doesn't make sense to pursue a language feature. |
I expect a source_gen based solution would look reasonable. You can generate
source_gen supports multiple generators on one file, no trouble. |
I hope so :) ... I think it's good enough for many problems, what's needed is for people to try it out and explore what works in practice. If we can suggest small language changes that make the codegen story better, that seems a good direction. |
Assuming the comparator is reified at compile time, I don't see why it wouldn't take the same amount of memory as today. |
I don't understand. How would it work with Comparator then? |
Oh, I see, you're basically passing in a method to the That doesn't help our general case, unfortunately. We have lots of classes in the Flutter framework that compare objects and rely on the |
Sure. We can do that today, no need for any new syntax or anything. It's significantly uglier than just having an |
That page just says it's hard (and then gives some Java-specific issues). Our experience is not that it's hard, only that it's tedious. Ideally the solution wouldn't involve writing code for both hashCode and ==. Exactly how it is implemented isn't something I'm particularly concerned about, so long as it is fast and the syntax is brief, convenient, and readable. |
In The
|
@tatumizer wrote
Doh! :) Fixed |
It is a common Dart idiom to have a class with all-final fields, a const constructor, a toString, an operator==, and a hashCode function.
The latter two are boilerplate:
It would be nice if there was some syntax or shorthand we could provide that would automatically provide these.
cc @abarth
The text was updated successfully, but these errors were encountered: