-
Notifications
You must be signed in to change notification settings - Fork 209
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
Awkward two-constructor necessity in extension type with validation #3343
Comments
IIRC, you can always just cast into an extension type |
If this is the case, is there a way to validate when casting? If one can simply cast into an extension type, it doesn't seem too useful for validations. |
@srawlins wrote:
I hope, if and when we actually introduce primary constructors to the language in general, that the language team will be willing to generalize extension types such that it is possible to omit the useless primary constructor E._. One reason why the syntax is so inflexible at this time is that we did not want to commit to a large subset of the syntactic decisions about primary constructors as part of the extension type feature. However, the very limited syntax offered by @jakemac53 wrote:
True, extension types are not reified at all, and that's a very fundamental decision about the nature of this mechanism. So if there is an object I still think it makes sense to have validation code in extension type constructors: Every time an expression of an extension type It's not going to be an absolute guarantee. For example, we could have an expression like However, constructor based validation of extension types is certainly as strong as having an There's some discussion about a relevant lint in this issue and this comment. In short, I believe it makes sense to offer the trade-off to developers who want some validation:
@mateusfccp wrote:
You can check out the notion of 'Protected extension types' here. This is a very old proposal about extension types, and I played around with the idea that a cast to an extension type (and other type tests) should be associated with the execution of user-written code (validating the object as having that extension type). One difficulty with that idea is that hot reload will traverse the heap and perform type checks to ensure that the updated program will proceed in a sound state, and that operation cannot allow execution of arbitrary Dart code. Another point is that this kind of mechanism must definitely be "pay as you go" (so we can't allow any code to be more costly in time or space if it doesn't use this feature). Suffice it to say that it is a delicate exercise to support user-written validation as part of a type test or type cast operation. On the other hand, it's obviously possible to declare an The constructors of
You won't get a guarantee. However, I don't think it's reasonable to say that a validation regime is useless if it relies on some conventions. Again, every time you do invoke the validation code you will get the validation, and if the loopholes are basically "cast to After all, the C++ community hasn't abandoned the C++ type checker, in spite of the fact that they can just cast an arbitrary memory area of the relevant size to any type whatsoever. That's a pretty heavy amount of evidence that static checks can be useful, even in a situation where there are no absolute guarantees. |
Being able to cast to the extension type without any constructor call is very interesting. This issue is not so much about having an ironclad guarantee that validation is run; maybe a lint rule against casting is good enough for me. The issue is just about the awkwardness of having to declare a constructor that you don't want to be available, even in the declaring library. Short of new syntax, we could encourage you to declare the primary constructor as |
If we think we'll be able to omit the primary constructor in extension types in the future, then I'd be hesitant about creating a "temporary" convention to handle that case (because it would be very hard to stop supporting it). At least making the constructor private limits the exposure to bugs to the declaring library. |
The So the question is basically (1) do we get primary constructors, including the variant which is declared in the body? .. and (2) do extension type declarations get to use them in their full generality? If we do get that then we can just write it as @bwilkerson suggested: extension type DegreesKelvin {
primary DegreesKelvin(double degrees) {
if (degrees < 0) throw ArgumentError('must be positive');
}
} |
If you think of the header-part of an extension type as part of its syntax, more than as a constructor declaration, then it might be more palatable for you. You get the this constructor whether you want it or not, because it's really not there. It's a no-op constructor which does nothing but statically cast the argument value to the extension type. There is absolutely nothing remaining of that "constructor" at runtime. That no-op constructor also serves as a reminder that someone can always create an expression with the extension type as static type, and any value of the representation type as (representation) value. If you don't want to expose that as a public constructor, because you want a different API, just mark it private. It doesn't change that anyone can do |
We don't need (and we actively want to avoid) a specific constructor declaration, but we must declare it. However, we can give it a private name, and then it's almost gone. Why not just omit the unwanted constructor in the header?
Consider an expensive porcelain vase. Most likely, it is possible to break it. However, I don't see how it can help anyone to insist that there must be a hammer right next to the vase at all times. Are you saying that it's dishonest to pretend that the vase is not breakable, so we must set up things such that it is very easy to break it? (OK, you can wrap the hammer in a piece of private paper, and then nobody will see it.) But why isn't it OK to just omit the hammer, and try to be careful and not break the vase? |
If it's a private hammer that only you can access, it's a great reminder that vases are breakable, and a way for you to break it, should you really need to. Or maybe it's just a bad metaphor. We can definitely allow other ways to declare, or not declare, a default no-op constructor and the representation variable. Maybe a different way to declare the representation type as well. If we say that you don't need to get a constructor, even if you can make it private, the same argument applies to having a representation object getter. We can omit that too Say, version 1: extension type Foo {
int;
} If, and only if, there is no "primary constructor"-like syntax in the declaration header, the first entry of the extension type declaration body must declare the declaration type. It can be just the type, or it can be extended with a name, and it can be prefixed by extension type Foo {
int foo;
} It's not a variable declaration, it's a special syntactic form which must occur first in the body, like enum values in an Then you can declare your own constructors. If you don't, there is no constructor, and you have to rely on casting to get into the extension type. (We won't introduce a "default constructor" if you already opted out of the normal default/primary constructor.) extension type Foo {
int foo;
Foo(this.foo) : assert(foo > 0);
factory Foo.foo(int foo) => foo > 0 ? foo as Foo : (throw "Bad argument, bad!");
} A generative-constructor-like extension type constructors can use the syntax for field initialization to select the representation object that the constructor returns. Or, version 2, we keep the representation type in the extension type header, but don't introduce a constructor unless the type name is prefixed by extension type Foo(int) {
... works like extension type new Foo(int) {
... to introduce a no-op constructor, Same affordances about naming or not naming the representation value. Definitely possible, but I'm not sure the complexity is worth it, since all you really need to do is: extension type Foo._(int _foo) {
} and then nobody else needs to know about the no-op constructor and representation object getter, which are really just aliases for no-op casts (I personally prefer to do something like: extension type Point._(({int x, int y}) _coords) {
Point(int x, int y) : this._((x: x, y: y));
int get x => _coords.x; // Would be `int get x;` if we allowed that..
int get y => _coords.y;
} when declaring constructors. Having the private While I'm usually one of the first to complain about things in my API that I don't want to be there, this one really doesn't irk me at all. |
I think I am coming to this conclusion as well; these cures all look worse than the disease to me 😄 . |
@lrhn wrote:
In some cases I might be pretty sure I don't want to break the vase, and I really don't need a reminder. ;-)
But now you proceed to change a large number of rather fundamental elements of the mechanism (for instance, having a representation object with no name). I'm not suggesting that at all. I'm just suggesting that if we add primary constructors to the language then they should be applicable to extension types without special exceptions. @srawlins wrote:
It isn't a big thing, but I do think this version makes sense: extension type DegreesKelvin {
primary DegreesKelvin(double degrees) : assert(degrees >= 0, "must be positive");
} If we can write a primary constructor like that in a class then I can't see why we shouldn't be allowed to do the same thing in an extension type. |
I'll change this proposal to 'extension-types-later' because it is likely to be resolved by the introduction of a general primary constructor feature, if primary constructors are indeed added to the language. In particular, with a general primary constructors feature there is no need to specify a primary constructor at all if it is not the best solution for a given extension type. |
IIUC, when I want to validate a representation object, I can do that in a constructor in an extension type. But a primary constructor doesn't have a body, so that must be done as a separate constructor. I must declare two constructors then, and one of them must not be usable. Here's an example:
DegreesKelvin._
and side-step the validation.Possible solutions
Encourage validation through
isValid
instance methodsThis is how the example in the spec is written.
Declare primary constructor in body
@bwilkerson suggested a syntax like:
The modifier
primary
is, at this point, only a signal for the compiler to see which constructor declares the representation type and representation field name.No primary constructors; go back to declaring the single field explicitly, as in inline classes
Just what the heading says.
Allow a primary constructor to be "re-declared" / "re-defined" with a body
This would look like:
You could say the primary constructor is declared on the first line and defined in the body of the extension type. You could put in requirements that the parameter static type and name are the same as in the declaration. You could put in a lint that reports if such a constructor doesn't have a body or initializers (can just be the declaration on the first line).
The text was updated successfully, but these errors were encountered: