Open Reprevise opened 1 year ago
As written, there is nothing new to collection equality. The equality for value classes would work only because the value fields are unmodifiable, so if two value class instances are equal, they stay equal.
So if the collection literal expressions are constants, the list is canonicalized and equal to other similar canonicalized lists. If not, the list is mutable and not equal to any list other than itself. Nothing new there, no intent to change that.
We could have an intermediate case between constant and mutable, where we allow an unmodifiable list to be equal to another unmodifiable list with the same values, but that requires recognizing that the other list is unmodifiable, which isn't visible in the type, and even if it was, it couldn't be trusted.
The only way to know if two maps with equal keys and equal values are "the same" is to know what they're used for, and possibly how they are created. That depends on the use-case, and is not something that makes sense to hard-code into the language.
Inside the FooBar
class, it may know that a private Map<String, String>
value is safe to compare to another FooBar
's map, because nobody will change the map while you are checking, the maps use the same equality, and they mean the same thing to the surrounding FooBar
s.
Two arbitrary maps, from different places, may not mean the same thing even if their key sets and associated values are "equal". They might be maps of different types, with different equality and hash code strategies, and they may be updated in different ways in the future. There is nothing naturally equal about them, even if they currently happen to have very similar states.
So as written, in order for collections to have equality in value classes you need to either:
fast_immutable_collections
or another similar packageIMO Dart really needs immutable collections at a type level, but that's a separate issue and I'm sure there's already one open for it.
My 2c on the spec that was checked-in by @munificent:
Removal of const
in Flutter is a huge win (speaking as one who uses Flutter, and from the great analysis done on actual code shared in the spec) .. but since const
classes are deeply immutable, doesn't that require value
classes to also be deeply immutable, in order for 'implicit const
to be applicable? Otherwise further restrictions would have to be placed on field types in value
classes (i.e., no mutable collections, and no non-value
types). At that point, we are again broaching deep immutability - discussed in my proposal #3298.
Is const
the right way to describe runtime-constructed immutable objects? A value object (properly defined) is a class constructed at runtime that is canonicalize-able due to deep immutability, not due to the fact that it is constructed using compile-time constants. Thus, just as there is the concept of a const
context for constructing and assigning const
objects, there needs to be something akin to a value
context in order for the compiler to validate correct construction and assignment of value types. Keeping to the principle of brevity, something like val
seems appropriate here as the runtime-immutable counterpart to const
.
A value
class is a class that obeys value semantics - whose precise definition in the context of Dart should be agreed upon at some point - but which also has an identity - unlike records -, because it is a class with an associated class name. To illustrate, if two value classes unrelated by inheritance (TypeA
and TypeB
) both have a single int a;
field, an instance of TypeA
would not be equal (via the ==
operator) to an instance of TypeB
even if their a
fields contain the same number [1].
Want to also highlight that I have signed the Google CLA, which means my posts in the issue trackers are meant to be contributions and is IP that now belongs to Google, which can be leveraged by the team (or not) at their discretion. I only ask (and expect to be the case) that the team will do so in a Googley manner.
cc @munificent @lrhn @dart/language-team
[1] Similarly, two contributors who both add value to a discussion or proposal should both get some credit..but that's maybe besides the point ;)
Being const-compatible is enough for making instance creations constant when the arguments are constant. We don't have to require, or check for, get immutability, we just don't get constness when it's not possible.
The default today is to not be const unless you ask for it. Then it checks that the expression can be const, and it's an error if it cannot. The idea here is that we do that check first, and if it would succeed, we add the const for you.
It's not that simple, sadly, done expressions depend on being in a constant context for deciding whether they are constants or not. That's why a deeply nested []
is a problem. Og tit don't ask for const, and thereby introduce a cost context, it won't be a constant. And then all the objects around it also won't be constants, even if they could have been, if you had written a const
.
So there are expressions which can be const if they must, but won't, and can't, be if they mustn't. That makes the "be const if it can" less useful than it sound.
The default today is to not be const unless you ask for it. Then it checks that the expression can be const, and it's an error if it cannot. The idea here is that we do that check first, and if it would succeed, we add the const for you.
I see, - However, that's not a unique attribute of value
classes, since that check could be done for any class that has all of its fields declared final
. In fact, isn't all fields being final
a requirement for defining a const
constructor? Conversely, how would value
classes as described in the spec help with the nested []
issue, since it is only adding implicit final
for instance variables?
I'm not sure I fully understand the issue with a nested []
- is it an issue of inferring the larger const
context, or inferring that a specific []
is const
when it's not declared as one? For the latter issue, it seems like a list not declared as const
not being treated as a const
even when initialized with const
values, is in fact the right behavior.
What if type system can answer:
I think first one is already supported but inorder to enforce immutability we need the ability to answer the second one. Having everything final
is one way to enforce it but it's restrictive and cannot help in cases like const [].add(1)
. If second question can be answered, then I'll be able to enforce immutability on objects even if they cannot be const(will get const-ness too in many cases where it's currently not possible) and that'll help implement value/data/whatever classes.
(@rchoi1712, I didn't mean to disrespect anyone but I apologise if that's how it looked on the other thread)
The Dart type system is oblivious to mutation. It really has absolutely no notion of it. Nothing in the type tells you whether a value is immutable, deeply or shallowly.
The only exception is enums, which cannot change their state. (Doesn't mean an enum value cannot have a getter which returns different values each time it's called. Not all objects keep all their relevant state in the inside.)
@lrhn wrote:
The Dart type system is oblivious to mutation. It really has absolutely no notion of it.
Well, the Dart type system does have elements where mutation matters:
void main() {
int? x = 1;
() { x = 2; }; // Just create this function object and throw it away.
x.isEven; // Error, the _type_ of `x` doesn't have an `isEven`.
}
When the expression () { x = 2; }
exists, x
is marked as being write-captured, which implies that (in principle) it could get a new value at any point in time (we make no attempt to track where/whether that function literal is called). So promotion is turned off for that variable. If we comment out that line then the program has no error. It's the assignment in the body of that function literal that makes the difference.
The current work on promotion of private instance variables, #2020, relies on a number of requirements of a similar nature (you can't override the instance variable by a getter or redirect it to noSuchMethod
, etc.): It does matter whether or not a given getter (in this case: the implicitly induced getter of an instance variable) is known to return the same result when it is invoked multiple times.
@hamsbrar wrote:
What if type system can answer:
- Whether an object is immutable
There are a number of proposals where the property of being immutable is associated with a specific getter as an explicitly declared property. Check out stable getters (#1518), value fields (#3332), and the whole topic of field-promotion.
Immutability of an object could be considered to be a derived property: An immutable object is an object where every getter is immutable. Similarly for deep immutability. We could add further type system concepts for capturing these notions of immutability.
However, if you wish to use immutability as a starting point for reasoning about code correctness then you just need to know that the getters that you are actually calling are immutable.
You might want to have a much, much stronger property: That invocation of a given getter will yield the same result if it is invoked multiple times, and it has no side effects. We might call this a 'pure getter'. We could also have pure members of other kinds, like functions with no side effects whose return value depends only on the actual arguments (which are required to be pure?) and on pure properties of this
.
We could take this as far as we want, and it would certainly make many kinds of reasoning about programs simpler.
However, I tend to think that it is also quite restrictive, and Dart is so much about side effects that it makes more sense to allow side effects, and leave it to the authors and maintainers of member declarations to use them judiciously.
For instance, invoking a stable getter g
could have side effects, but it should not break anything if some client code changes ... g ... g ...
to var x = g; ... x ... x ...
(that is, if the client caches the returned value, knowing that the next invocation of g
will return the exact same result anyway).
I guess the main point is that "type checking" is generally about interfaces ("which methods does this object have, and how can I call them?") and data flow ("can I assign this expression to that variable/parameter?"), but static analysis can certainly take other statements about the execution into account.
For instance, there's nothing wrong with a static analysis that establishes certain conclusions about an expression being stable, or an expression having no side effects (if we want to go that far), or any other property that we can speak about and wish to know. It's just a matter of deciding what is useful and possible to know, not a matter of checking "whether or not it's about types".
However, I tend to think that it is also quite restrictive, and Dart is so much about side effects that it makes more sense to allow side effects, and leave it to the authors and maintainers of member declarations to use them judiciously.
That's true. By the way I meant the ability to enforce immutability on any object not every object, because that can help with identity and other parts of value/data classes. But even in that, it may not be as useful as I think it is.
For instance, invoking a stable getter
g
could have side effects, but it should not break anything if some client code changes... g ... g ...
tovar x = g; ... x ... x ...
(that is, if the client caches the returned value, knowing that the next invocation ofg
will return the exact same result anyway).
A current-getter cg
is definitely bad. Both var x = cg; ... x ... x ... x
and ... cg! ... cg! ... cg!
are unsafe but latter is the worst. There's no doubt that your-stable getter g
is correct. I'd want it to be default on my final fields even if the method used to test idempotency is an always on going effort. It maybe because I'm not using getters as much as others are.
The discussions(stable getters and value fields in particular), seems to solve the problem from the other end i.e going against the initial design by putting restrictions on what one can do in a field access. Treating fields as fields is as far as one can go in this direction and value fields seems to be going that far.
I see that getters are helpful and some people might want to keep them so I'm leaving some ideas here: https://github.com/dart-lang/language/issues/3358
The draft for value classes was just added by @munificent and I noticed that it didn't seem to mention collection equality (correct me if I'm wrong). I know the spec was just added but I figured I'd open an issue to get dialogue on this topic.
If I were to create instances like so:
Would these be equal even though they're not const?
Also will there be deep
Map
equality?