Why can't you create const objects without the class meeting the conditions, in Dart

89 Views Asked by At

To clarify, I am asking about the design decisions of the language and the internal workings of the compiler and executor.

In Dart, there are three conditions for creating a const object from a class:

  1. The constructor should be a const constructor
  2. Every attribute of the class should be final
  3. The constructor should not have a body

P.S. : I am going to self-answer this question. But I would appreciate if you can read my answer and tell me if it is wrong or add information that I did not know

Although not directly a part of my question, I am attaching below the series of tests that I did to undestand the language with respect to this question: https://docs.google.com/document/d/1Y8IlOlzI5VGFLzLFlih1d7QgOdynUxvrF7Q3kd0OSwQ/edit?usp=sharing

2

There are 2 best solutions below

4
Andhavarapu Balu On

My hypotheses:

We know that Dart creates different constructors for creating const and non-const objects (even constructors marked as const internally have a non-const copy). The compiler could’ve been made to create a const constructor on its own when it encounters a const object creation, but this leads to redundancy (both time and memory-wise) because of issues pertaining to the code segment memory allocation to the class and pre-compiled header files. So it is better for the class itself to mark the constructor as a const constructor, and hence the rule 1.

Restrictions on modifying attributes, is handled in the form of removing their setter methods. Methods are common for the class, and can not be separated between const and non-const objects without creating different versions of the class. Although, this is likely possible for the compiler to implement internally, Dart perhaps chose the simple approach of leveraging existing mechanisms, which results in rule 2. Because a final attribute does not have a setter method, whether the class has a const constructor or not.

For rule 3, There are two reasons why we might want to have a body for a constructor:

  1. Calculating the values of the attributes from the parameters before setting them. But to assign the calculated values to the attribute of the object being created, the setter function has to exist. But it doesn’t for final attributes. So this use case can not be applied.
  2. Creating side effects: But since the const object is created at compile time, what side effects are we expecting that we can’t hardcode ourselves? Besides, it restricts the compiler on the sequence of passes it makes and the way it compiles codes. Coders may also have wrong expectations of the flow of execution of the various const object creations with respect to each other and the rest of the code. So this use case is not to be applied either.

When neither of the use cases is applied (and both cases require additional work to implement, as normally in languages the constructor body is executed after creating the object), why even allow a body? If at all, we need something to be run from the object, we can do it at runtime with class methods (not the constructor)

0
lrn On

A Dart const constructor has a number of constraints, and it imposes some requirements on the class itself.

  • A generative non-redirecting const constructor cannot have a body.
  • It must call a generative const constructor of the superclass.
  • Initializer list expressions must be potentially-constant expressions.
  • Any instance variable in the class must be final.
  • Any initializer for an instance variable must be constant.

The question here is why a constructor and class which satisfies these requirements (and transitively, the superclass and its constructor being called also satisfying them) cannot be used to create a constant, even without the const.

The answer lies in those requirements.

If a class author has not promised that a constructor can be used to create a constant, then even if a constructor and class satisfies the requirements today, it's incredibly easy to change the class so that is no longer the case.

Such a change would be a breaking change, your const TheClass() constant will no longer compile. However, there is nothing in the class author's code that becomes invalid. They never intended the constructor to be const, so they never tested that it could be, or used it as const. So when they add a new non-final instance variable to the class, say to cache some computation, which doesn't change the public API at all, they test that their class still satisfies all their requirements, and all is well.

And your code breaks. Now, you can try complaining to the author and make them undo the change that they wanted to do, just so you can continue doing const TheClass(). They'll likely say no, and say "I never promised this could be used as const." And they'd be right.

To avoid this scenario, and many similar ones, a constructor, and class, does not support being used with const unless the author promises that they mean it. That's what being a const constructor means - that the author has authorized its use in constant expressions, and everybody can agree that it's a breaking change if that stops working.

The reasons for those restrictions existing to begin with is another question. The general design principle for Dart constant expressions are:

  • They can be evaluated at compile-time without executing any user-written statement (so there can be no loops!), and only a limited set of expressions which invoke operators on core platform types like num, String and bool.
  • Every constant expression evaluates to the same value every time it's executed. No side effects, no reading mutable state. That's why the value can be "computed" at compile-time - it cannot depend on anything that happens at runtime.
  • Constants can be canonicalized, so instances of the same class with the same state, can be represented by a single object.

To satisfy those principles, a constant constructor of a user-written class must:

  • Not have a body (no user written statements!).
  • Not call a super-class constructor which isn't safe (only call constant super-constructor)
  • Not have any mutable fields (non-final or late final), because then you cannot safely canonicalize.
  • Not have a an instance variable initializer which isn't constant (it's evaluated as part of the constructor invocation).

All of these restrictions exists to satisfy the design creterias for constants. Having to write const in front is a way to opt-in to these restrictions, both to prove that you mean it, and to then get an error if you make a mistake and no longer satisfy all the restrictions. Without the opt-in, it couldn't be an error for the class itself to not satisfy the requirements.