Skip to content

const Constructor parameters are never considered const #2000

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

Open
ltOgt opened this issue Dec 1, 2021 · 15 comments
Open

const Constructor parameters are never considered const #2000

ltOgt opened this issue Dec 1, 2021 · 15 comments
Labels
enhanced-const Requests or proposals about enhanced constant expressions request Requests to resolve a particular developer problem

Comments

@ltOgt
Copy link

ltOgt commented Dec 1, 2021

If a class defines a const constructor, it can be created either as const or as regular internally final object.
Should this const-in-const-context notion not apply to the parameters as well?

Take e.g.:

class CanConst {
  final int value;
  const CanConst(this.value);
}

Here const v = 1; const CanConst(1) works but (int v) => const CanConst(v); does not.
=> parameter must be constant when used in constant context.

Where as const v = 1; CanConst(1) and (int v) => CanConst(v); both work.
=> parameter must not be constant when used outside constant context.

==> parameter behaves like it is const-in-const-context


Since the parameter is currently not const-in-const-context, we get e.g. the following:

const canConst = CanConst(1);
const canNotConst = CanNotConst(1); // Error
const canNotConstContainer = CanNotConstContainer(CanNotConst(1)); // Error

class CanConst {
  final int value;
  const CanConst(this.value);
}

class CanNotConst {
  final int value;
  CanNotConst(this.value);
}

class CanNotConstContainer {
  final CanNotConst cnc;
  const CanNotConstContainer(this.cnc);  // No Error
}

Which I think should be warned about in const CanNotConstContainer(this.cnc) since CanNotConst will never be able to be const.


Further, we get counter-intuitive (at least to me) behaviour with trying to make classes constant:

Starting with this non-constant class:

class MyClass {
  final int value;
  final MyOtherClass other;

  MyClass(this.value, this.other);
  MyClass.sameOther(int value)
      : this.value = value,
        other = MyOtherClass(value);
}

class MyOtherClass {
  final in value;
  const MyOtherClass(this.value);
}

We can make the default constructor const since MyOtherClass has a const constructor:

class MyClass {
  // ...
  const MyClass(this.value, this.other);
}
const myConst = MyClass(1, MyOtherClass(1));

However, we cant do:

class MyClass {
  // ...
  const MyClass.sameOther(int value)
      // why does this work ...
      : this.value = value,
        // ... but this does not
        other = MyOtherClass(value);
  // ----------^^^^^^^^^^^^^^^^^^^^^
  // Invalid const value
}

Since the const part of the constructor is only used in const context, I had the following mental model:

Used in const context.

class MyClass {
  const int value;
  const MyOtherClass other;

  const MyClass(this.value, this.other);
  const MyClass.sameOther(const int value)
      : this.value = value,
        other = const MyOtherClass(value);
}

Used outside of const context.

class MyClass {
  final int value;
  final MyOtherClass other;

  MyClass(this.value, this.other);
  MyClass.sameOther(int value)
      : this.value = value,
        other = MyOtherClass(value);
}

P.S.
I recently assumed that fields of an object created via const would be const as well, which might be related (even if only in my wrong mental model)
(#1868 (comment))

@ltOgt ltOgt added the request Requests to resolve a particular developer problem label Dec 1, 2021
@eernstg eernstg added the enhanced-const Requests or proposals about enhanced constant expressions label Dec 1, 2021
@Cat-sushi
Copy link

A duplicate of #823, in some way.

@eernstg
Copy link
Member

eernstg commented Dec 1, 2021

Note that it is slightly misleading to say that

CanNotConst will never be able to be const.

You can have a class with a constant constructor that implements CanNotConst:

const canConst = CanConst(1);
const CanNotConst canNotConst = CanNotConst2(1);
const canNotConstContainer = CanNotConstContainer(CanNotConst2(1));

class CanConst {
  final int value;
  const CanConst(this.value);
}

class CanNotConst {
  final int value;
  CanNotConst(this.value);
}

class CanNotConstContainer {
  final CanNotConst cnc;
  const CanNotConstContainer(this.cnc);  // No Error
}

class CanNotConst2 implements CanNotConst {
  final int value;
  const CanNotConst2(this.value);
}

This is the part which is a duplicate of #823:

class MyClass {
  // ...
  const MyClass.sameOther(int value)
      // why does this work ...
      : this.value = value,
        // ... but this does not
        other = MyOtherClass(value);
  // ----------^^^^^^^^^^^^^^^^^^^^^
  // Invalid const value
}

@ltOgt, perhaps you could extract the non-duplicate parts of this issue into a new issue?

@lrhn
Copy link
Member

lrhn commented Dec 1, 2021

The issue here is that const C(int v) : x = D(v);, where D has a const constructor, means const C(int v) : x = new D(v);. That's not valid inside a const constructor.
If you instead write const C(int v) : x = const D(v);, then it's not valid because the potentially constant v cannot occur inside the definitely constant const D(...). It wouldn't work if C is invoked with new.

We currently have no way to write auto D(v) which would be const when the C constructor invoked as const and new when it's invoked as new. That would solve the problem.

It would break one assumption that Dart has so far been able to have: That every constant is created by a specific position in the program, and any constant expression creates precisely one constant value. That's what ensures that there cannot be more constants in a Dart program than linearly in the program size. (That's also why we can definitively disallow cycles in const constructors, because we are absolutely certain that coming back to the same constructor will hit the same const creation again.

So, all in all, it's not currently possible. It's a valid improvement to have that feature, but it's not without risk.

@ltOgt
Copy link
Author

ltOgt commented Dec 2, 2021

Note that it is slightly misleading to say that

CanNotConst will never be able to be const.

You can have a class with a constant constructor that implements CanNotConst:

You are right, I did not think about that.

This is the part which is a duplicate of #823:

class MyClass {
  // ...
  const MyClass.sameOther(int value)
      // why does this work ...
      : this.value = value,
        // ... but this does not
        other = MyOtherClass(value);
  // ----------^^^^^^^^^^^^^^^^^^^^^
  // Invalid const value
}

Yes, this is what my issue boils down to, @lrhn did a better job bringing it to the point than I could.

Maybe plus the note that the current const behaviour is a little counter intuitive.

  • Can't use const constructors in const constructors
  • Can't use const instance fields in const expressions (the other linked issue)

Both of these things feel natural to assume at first.
It feels weird that e.g. this does not work:

void main() {
  const a = A("b");
  const b = a.v;
}

class A {
  final String v;
  const A(this.v);
}

It might be nice to have a little more documentation on const constructors e.g. at https://dart.dev/guides/language/language-tour#constant-constructors

@Levi-Lesches
Copy link

It feels weird that e.g. this does not work:

void main() {
  const a = A("b");
  const b = a.v;
}

class A {
  final String v;
  const A(this.v);
}

This is a whole separate issue. The assumption in your code is that because a is const, a.v should also be const. There are two reasons that may not be true:

  1. A.v may be a getter (String get v => "Hello, ${getUsername()}";)
  2. Even if A.v is a simple field, a may be a subclass of A that overrides A.v

@cedvdb
Copy link

cedvdb commented Dec 3, 2021

Can't the analyzer figure out both 1 and 2 ?

@Levi-Lesches
Copy link

Dart doesn't have a difference between a field and a getter. A field is simply sugar for a getter + setter combo, and a final field is sugar for just the getter. So I don't think so.

@ltOgt
Copy link
Author

ltOgt commented Dec 3, 2021

This is a whole separate issue. The assumption in your code is that because a is const, a.v should also be const. There are two reasons that may not be true:

Yes sorry, this was actually #1868.
I just wanted to point out that both these cases are not intuitive.
I now understand why const currently behaves like this in both cases, but feel like this could be documented better

@Levi-Lesches
Copy link

Well that case is more complex than this one. You had an interesting question in this case. Since const expressions are known at compile-time, shouldn't the compiler be able to determine whether a is A vs a subtype, and whether A.v is a getter or a simple field? I honestly don't know the answer, and just gave what made the most sense to me. I'll ping @eernstg and @lrhn -- and I'm predicting that at least one of them would say that the answer to those questions shouldn't matter, as depending on that behavior makes changing it a breaking change where the author of A never committed to it.

The answer to your other case is a bit simpler, and you already said why: while the static type of widget may be known, its runtime type isn't. Which means it can easily be an XWidget or a YWidget, or maybe even a ZWidget based on something that happens during runtime (data from a database, user input, etc.). In other words, the compiler needs to be ready for the worst possible case since there's no way for it or a human to know what will happen. #1518 specifically introduces a type of getter that declares itself as side-effect free. This restriction would be inherited, which should solve a whole class of promotion problems at the root.

@eernstg
Copy link
Member

eernstg commented Dec 3, 2021

@cedvdb wrote:

Can't the analyzer figure out both 1 and 2 ?

'2' is the property that the statically known declaration of A.v (a final instance variable) is not overridden by a getter in a subclass.

A closed-world analysis can iterate over all class declarations and check this property, but that's not an acceptable constraint on a programming language: If we're writing a reusable library L then the subclass with this override may be written in a library L2 that we've never had any knowledge about, and there could then suddenly be a compile-time error in L because of code in L2. This means that libraries can't be reusable.

@cedvdb
Copy link

cedvdb commented Dec 3, 2021

Sorry I deleted my comment as I felt like the question fit more in the if variable issue but to give context to your answer it was:

Wouldn't a better solution be to make the analyzer differenciate between a getter which returns the same value on sequential calls and when it might not be the case to allow for field not null promotion in if( widget.something != null) and possibly this issue ?

@eernstg
Copy link
Member

eernstg commented Dec 3, 2021

Shouldn't the analyzer make the difference between a getter with side effects and when there is no side effects

The important distinction is not with/without side effects, it is whether we have or do not have a guarantee that the getter will return the same value each time. #1518 is aimed at that exact goal.

@eernstg
Copy link
Member

eernstg commented Dec 3, 2021

@Levi-Lesches wrote:

Since const expressions are known at compile-time, shouldn't the
compiler be able to determine whether a is A vs a subtype

This is an interesting question. A compiler could rely on properties of the actual value of a constant expression (in particular, we can know that a.v will evaluate the implicitly induced getter on an instance of A, not an overriding getter like the one in B).

I guess the most important reason why I wouldn't like to go down that path is that a large software system derives a lot of software engineering value from complexity reduction mechanisms, and this kind of detailed implementation-dependent reasoning is a violation of encapsulation, and it breaks the complexity reduction mechanisms.

If we develop and maintain the software system relying on properties that are associated with interfaces (such as the types of expressions, and the members known to exist for such types, and the types of those members) then, ideally, we don't have to worry about the underlying implementation. However, types are a very rough approximation of a specification of the semantics of the software, so we will have to rely on a lot of informal information about the behavior of the software, in addition to the type/interface based reasoning.

Dart does not have non-overridable method implementations (like final methods in some languages), and Dart guarantees that object storage is firmly encapsulated (because instance variables cannot be accessed by Dart code, we can only read and write them by calling getters and setters, and the implicitly induced getter/setter pair for an instance variable will do the raw memory read/write that we cannot express directly).

This means that Dart's meta-contract with us as developers is that (1) we are allowed to create an implementation of any given interface afresh (implements), or reusing parts (extends, with), and we can adapt any member of the interface as needed by overriding its implementation. Conversely, (2) we cannot obtain the guarantee that any particular member has any particular implementation.

However, (1) is applicable to software evolution as well: When a given class is modified over time, it is known that clients cannot depend on the encapsulated properties, and this means that we are free to modify those properties (e.g., changing a stored property to a computed property, or vice versa).

If we allow constant expression correctness to rely on knowledge about some of these encapsulated properties then they can't be modified any more. Crucially, we can't know whether there is a client who is depending on any particular property, because it might be in someone else's code, in a package that imports our code.

So you'll have to be very careful about making a constructor const, because this might be a heavy burden on the evolvability of the enclosing class.

My conclusion is that we shouldn't break the encapsulation, because that's expensive at the level of software engineering, even though it might look benign in the context of a small snippet of code.

However, it's a completely different matter if we want to achieve a similar result by means of an interface property: If we have something like the stable/final getters of #1518, then we can declare that a particular getter is guaranteed to return the same object if it is invoked twice on the same receiver. Then we have a different contract with subtypes (they must maintain that property as well), but they are still free to override the implementation as needed.

So my answer is: Yes, we can know that a getter of a constant object is constant, but we need to do it right.

@lrhn
Copy link
Member

lrhn commented Dec 3, 2021

Can't the analyzer figure out both 1 and 2 ?

Absolutely (as Erik says, with a closed world assumption, it can know everything about the program structure).
But it's not (just) about the current program's structure, it's about being about being able to change the structure in the future.

If the compiler does figure out that it's a final field of a const class, and allows you to access it in a const expressiion, then it means that the author of the class can no longer change their instance field to a getter. That would break the code that the compiler allowed only because the getter was backed by a field. We'd lose getter/field symmetry, which is the entire reason for having getters to begin with.

Dart does not allow you to distinguish between a getter written using get and one induced by a field/variable. Any place it would do so, would be a place where you could no longer change between the two.

We'd have to introduce something like constant instance fields, fields that are like final fields, but are usable in const expressions, and where the author promises to keep it that way in the future. The author needs to opt in.

Adding a language feature just to allow accessing fields of const objects has not been a priority so far. It doesn't happen often enough.

@Levi-Lesches
Copy link

and I'm predicting that at least one of them would say that the answer to those questions shouldn't matter, as depending on that behavior makes changing it a breaking change where the author of A never committed to it.

If the compiler does figure out that it's a final field of a const class, and allows you to access it in a const expressiion, then it means that the author of the class can no longer change their instance field to a getter. That would break the code that the compiler allowed only because the getter was backed by a field.

This is exactly what I had hoped for when I clicked "watch all issues on repository" a few months back :)

Thank you both for the detailed write-up, my interest in stable getters has gone up considerably now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhanced-const Requests or proposals about enhanced constant expressions request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

6 participants