-
Notifications
You must be signed in to change notification settings - Fork 213
Allow late final fields on const classes #2225
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
How would a compiler encode a constant object with an uninitialized field?
|
That's correct. It's still |
The way Dart constants are currently defined, constant evaluation doesn't have to happen at compile-time. Constant expressions have three properties:
The specified semantics are such that you can't tell whether an object was created at compile-time or at run-time, and the run-time canonicalized. Early versions of the Dart dev-compiler did just that, and it wasn't wrong. (Well, not entirely true, since we require throwing operations to be compile-time errors, and we no longer support compile-time errors at runtime, like we did in Dart 1. The specification doesn't preclude it, though.) We don't have a notion of "potentially constant expression" which we can apply to the initializer expression, so either it must be just plain constant (in which case there is no need for But having a late final field breaks deep immutability. It also complicates the combination of the other two properties: If const objects are canonicalized at run-time, will the state of the When we send constant objects to another isolate, they are deserialized into the corresponding constant on the other side. That means that an initialized late final field is either not passed to the other isolate, since it already has the corresponding object, which may not have initialized the field, or if the isolates share the object, then initialization is a cross-isolate event. (That's not going to happen, we'd have to have cross-isolate mutexes to avoid seeing intermediate values, being immutable is what makes sharing easy. So, no cross-isolate sharing of those non-deeply immutable objects.) All in all, it's not clear what a If you want the same effect, just use an expando. Instead of static final Expando<Foo> _foo = Expando();
Foo get foo => _foo[this] ??= initializer; Then it's absolutely clear that the mutable property is not actually part of the immutable object. |
I think you can make it work with isolates if you specify that a) (a) is mandatory for sharing, (b) is optional - you can implement reasonable semantics without it. Unfortunately (a) also means that you are relatively limited in what you can do with it because you can't necessarily produce |
Yea, it might be a bit of work but if we had this feature we could create the memoization of (Related but meandering observation: I couldn't share logic between const constructor initializers yesterday either because once you pull an expression into a function Dart lost the idea that it was a pure function that could be evaluated at compile time).
Thanks, I'll look into that. I'm not sure what its performance is since the point of the exercise is to make things faster. I'll give it a shot. Thanks for looking into this. I don't have evidence this requires any large engineering effort. I want to put it on your radar as a limitation I ran into while trying to optimize Flutter. |
Using |
I don't think this is true. There are a number of specified compile time errors that rely on constant identity being available at compile time (e.g. having the same keys in a const map, or in a switch). |
@leafpetersen Well, there is that. The specification is rather vague, though. We do not provide compile-time semantics for constant expressions. They are evaluated using the same dynamic/runtime semantics that is used to evaluate expressions at runtime, since it's is the only evaluation semantics we have. The language semantics explicitly say that constant expressions are evaluated at runtime! They then also say that it's a compile-time error if some part of that evaluation would have caused an error. All compile-time "constant evaluation" has to do is detect whether the run-time evaluation would throw, or a potentially constant expression would fail to be an actual constant expression, or - as you mention - a constant map having the same object as keys, because those things must be reported at compile-time. We have restrictions on the constant expressions such that the you cannot tell whether we pre-computed the result at compile-time, or re-computed the result at runtime and then canonicalized at runtime, as long as the constant expression does not cause a compile-time error. Because that's what the semantics actually say that the behavior should be equivalent to, since that's the specified semantics. Evaluation doesn't have to happen at compile-time. Some "will this constant expression cause a compile-time error" analysis is needed, and in many cases it's probably indistinguishable from actual evaluation, but the actual evaluation which creates the runtime result can happen at run-time. |
I have to admit, you lost me somewhere in your response. I don't understand what you're trying to say, or if I do, it feels like a distinction without a difference. It's true that I can choose to evaluate constants at compile time, throw away the result, and then recreate the object at runtime but... so what? The point is, I do in fact have to evaluate the object at compile time. I think we agree that the compiler must evaluate at least some of the constants in the program in order to produce the specified errors, right? I don't really understand where you're going with the rest. Given the original definition of |
@leafpetersen wrote:
There is one property which hasn't been mentioned so far, which is crucial in order to determine which ones. The required compile-time errors arise with expressions that are 'required to be constant':
It might be possible to perform a static analysis on constant expressions such that these situations can be precisely predicted without actually evaluating the expressions. For instance, it might be possible to find a useful set of expressions where it would be guaranteed via static analysis that an evaluation at run time will not throw. But it doesn't seem worthwhile to do that, in particular because those expressions are probably the simplest ones. So, essentially, we're requiring compile-time evaluation of constant expressions in the following situations. The given expression is:
We have a "recursive" case, too, where the required evaluation propagates to other locations (and even to expressions that aren't present in the program):
This means that the set of constant expressions that must be evaluated at compile-time is quite substantial. Of course, we still have a lot of constant expressions that aren't required to be constant (such as void main() {
print(identical('ab', 'a' + 'b')); // 'false' on the vm, 'true' on the web.
} So we might wish to be stricter on requiring all constant expressions to be evaluated at compile-time. Note also that the particular case with canonicalization of strings is the topic of #985. |
I believe it's fair to say that the main topic here is caching getters on objects that are obtained as the value of a constant expression (let's just call them 'constant objects'): A late final instance variable with an initializer is essentially a cached getter. @lrhn already mentioned several reasons why we can't allow arbitrary computations at run time to provide the value of an instance variable in a constant object, even though the object will still "look constant" because the value is filled in at the first usage and then never changed. However, we can still consider some mechanisms that are a bit less permissive. For instance, we could consider introducing constant instance getters. An instance getter marked A constant instance getter is a compile-time error unless the enclosing class has a constant constructor. It is a compile-time error to override a constant instance getter by an instance getter which is not constant. During creation of an instance of a class During constant expression evaluation at compile time, this implies that We would need to consider non-termination (which is currently not possible during constant expression evaluation); it could be an error during constant expression evaluation if it ever evaluates an expression obtained by substitution into the expression Note that this idea is quite similar to 'user-defined constant functions', but not quite as powerful because the extraction of the value of a constant getter isn't (currently) a constant expression itself. |
I'm not seeing what "constant instance getters" can do that you can't do by just having a final field and putting the same expression into the initializer list. The only values the field expression can access are instance variables of the same current class (I'm not allowing super-class declared instance variables, that will break encapsulation and field/getter symmetry). The values actually stored into instance variables are all available in the constructor (not necessarily easily accessible, but simply allowing access to instance variables prior in the initializer as final local variables would fix that, and be generally useful, otherwise we just need one constructor level of indirection.) |
I don't think we can expect the expression to be a constant expression though A cached hashCode would typically call |
You wouldn't be able to customize an expression in an initializer list of a constant constructor (or leaving parts of it abstract) by overriding a declaration in a subclass. |
I don't believe there is a practical reason to disallow
late final
fields on const classes. It was probably disallowed to make the implementation easier (possibly to avoid thread synchronization?). I ran into this limitation when trying to memoize an expensivehashCode
on a const class. I think the only alternative I have for memoizing thehashCode
is to calculate it up front which unfortunately will have a different performance characteristic than the original code.code
related issue: dart-lang/sdk#48948
cc @lrhn
The text was updated successfully, but these errors were encountered: