-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Strong mode const semantics don't allow for compile time canonicalization #26291
Comments
There is the in-between option of allowing type variables in potentially compile-time constant expressions (and also allow other constructor parameters as variables), but not in general. In this case we have:
This is similar to the classical code:
in that it uses a constructor parameter in a compile-time constant expression. Neither is allowed by the current specification, both could be allowed without breaking anything (I think, but as usual, I might just be wrong). It's still not runtime canonicalization - the constructor only works when const invoked, you can't do
(Allowing |
Remove I'd rather have features to make all immutable values fast, even ones known only runtime. C# does this with structs, and we can probably do something along the same lines. |
We have to be very explicit about the underlying dynamic semantics. There's a semantics (1) where a type argument can be inferred and then reified, and a semantics (2) where omitted type arguments get supplied implicitly as With strong mode semantics we need to take inferred elements of programs just as seriously as explicit ones. This means that Leaf's second example should be considered to be an abbreviation: class A<T> {
const A() : x = const []; // Exactly the same thing as `const <T>[]`.
final List<T> x;
} Every invocation of the const constructor above will pass a type argument (possibly inferred), because every instance of class B {
const B(int i) : x = i; // Not using `this.x`, to make the similarity obvious.
final int x;
} The reason why This means that the actual conflict here is between the (lacking) generality of So const could be made more general, or it could be dropped altogether. We discussed generalizations several times; I think it would be nice to generalize const to give it a more "natural" set of limitations, which may or may not be considered to be a simplification, depending on your taste. ;-) We should remember, though, that "compile-time-ness" is essential in one case, namely for metadata annotations. So if we want to have metadata then we need some way to specify compile time values. It's worth noting that Java has an entire micro-language just for this purpose (https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.7). I actually think that a relatively manageable mechanism like But it would be really nice if we could exploit the existing notion of 'potentially constant' expressions to obtain good and smooth support for the construction of objects (and object trees) which are explicitly marked as immutable, and maintained as such by the language. |
That's probably going to be guaranteed by anything we chose to do because the annotation expressions are not in scope of any runtime parameter. |
(indeed, but I was thinking about the proposal to drop const entirely) |
Yup, this is a good point. My hunch is that we could simplify the set of expressions allowed in metadata annotations without too much loss in usefulness.
C# has something similar:
And:
It's quite limited—basically literals of primitive types and single arrays of such—and yet in my years using them I can't recall it feeling like much of a restriction. That leads me to think const in Dart is overpowered for the metadata use case.
Yeah, I agree it's more integrated—it shows up in things like default values, you can declare named constants, etc.—but it adds a lot of complexity to get that, and I don't see that we get that much value from it in return. It almost lets you do real compile-time programming, which would be useful for many things, but isn't powerful enough for that. It ends up leading you down a path where you think you can make something const, but will likely end up hitting the wall later. (Recall, for example, how RegExp used to have a const constructor, which then later had to be changed to non-const.) And we pay a high price for that just-slightly-more-powerful-than-Java-and-C#-ness: forwarding constructors, canonicalization, constructor initialization lists, I think we could come up with something that had a better power-to-weight ratio than that. |
So default values should be evaluated in static class scope and otherwise not be different from any expression that might appear in the body of a static method? That would be somewhat more powerful, and presumably benign. And we wouldn't need |
One problem that const values avoid is evaluation order dependencies. class C {
static _ctr = 0;
foo([int x = _ctr++]) => x;
bar([int x = _ctr++]) => x;
}
main() {
print([C.foo(), C.bar(), C.foo()]); // prints 0, 1, (0 or 2) ?
} I still think that a syntax for side-effect free and evaluation order independent values is useful. |
My cavalier answer is that we should get rid of default values too. :) I do see their usefulness, but they break forwarding in such ugly ways, pollute interfaces in confusing ways, and don't play nice with non-nullability. Maybe they are a necessary evil, but I'm not convinced they carry their weight either. In my own personal code, I almost never use them. (And, when I do, more often than not, I end up having to remove it later in order to get forwarding to work again.) I would expect your program to print Don't get me wrong, I see the value in having side-effect free code, but it does feel kind of bolted on to a language that is otherwise deeply imperative and mutable. If we want to start caring more about being side-effect free, I think we should have a more comprehensive immutability story. Limiting your side-effect free language to only be able to compute on values known at compile time is just too limited to be worth the language complexity, I think. |
That would mean that optional parameters cannot be non-nullable. Which does make a kind of sense. You managed to convince me in the time it took me to write a response :) |
Yup!
Yes, and interfaces. Having to document it is kind of a chore, but I still would prefer that over the fact that right now subclasses have to actually repeat the default value. That violates DRY for me, which I care a lot about. |
About the idea that a default value could be similar to an expression in static scope: Sure, there would be a potential for side effects and order dependencies, but Dart code in general has that everywhere, and we generally trust programmers to be able to handle it. I think this is a typical trade-off between emphasizing constraints and security vs. emphasizing flexibility and expressive power. Dart typically sets out offering a lot of flexibility, and then there are ways to add constraints to that. So could we do that? Yes: We should be able to support things like "this function has no side-effects" ( |
About broken forwarding with default values: How much would it help to have hints for the cases where not all named arguments are passed? There could be a In the general case it's hardly reasonable to require extra ceremony whenever an optional parameter is omitted, but we could of course add mandatory named parameters (whose semantics for dynamic (statically uncheckable) invocations would be like a default parameter whose default value throws .. ah, darn, then we need default parameters with a dynamic semantics, but I won't argue against that ;-). |
FYI: See the annotation |
About default values polluting interfaces: I think that they are part of the interfaces, because it is semantically significant exactly which value will be chosen for the given parameter when nothing is specified at call sites, and it would be a serious violation of encapsulation if the caller had to know the exact type of the receiver in order to be able to specify an explicit argument which would have the same effect. But this means that it should be a (fatally smelling) hint to give a different default value for the same parameter in an overriding method declaration. The interface is now bigger, because it includes the default values, but I don't think that there is any complexity there which is particularly confusing: It takes up more brain space (and more source code) because it does more. You could even claim that void foo({int someName: 5}) {
..
} is so much more informative than void foo({int someName}) {
someName ??= 5;
..
} that the former should be preferred, because it is relevant to the caller what an omitted parameter means. |
Ah, sorry: Didn't know about |
Actually, it's a warning specified by the specification in section 10.1:
|
Ah, missed that one, too! Thanks for the clarification. |
The solution is to infer a You still can't use type variables in a const expression. |
Moving this here from dart-archive/dev_compiler#503 since it's really a language issue rather than a DDC issue. See discussion there for some background and examples.
Because Dart 1.0 is defined to allow compile time canonicalization of constant expressions, const expressions are not allowed to be instantiated with type variables. This code is therefore invalid:
In Dart 1.0 the type parameter can be left off, since
List<dynamic>
can masquerade asList<T>
for anyT
.In strong mode, a
List<dynamic>
cannot be used as aList<T>
. Strong mode currently allows a type parameter to be filled in via inference forconst
objects such as these in order to continue to type as much existing code as possible. DDC implements this via runtime canonicalization. As we move towards unifying strong mode across platforms, we need to decide what to do here going forward. I see two immediate options:Are there other options?
The text was updated successfully, but these errors were encountered: