Comments (12)
@srawlins wrote:
I must declare two constructors then, and one of them must not be usable.
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 <representationDeclaration>
is likely to be a subset of any upcoming primary constructor feature.
@jakemac53 wrote:
IIRC, you can always just cast into an extension type
int as DegreesKelvin
?
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 o
with run-time type int
, and int
is the representation type of DegreesKelvin
(or a subtype of that representation type) then o as DegreesKelvin
will succeed (it won't throw), yielding an expression of type DegreesKelvin
without ever executing any constructor bodies. Similarly, o is DegreesKelvin
can promote o
.
I still think it makes sense to have validation code in extension type constructors: Every time an expression of an extension type E
is obtained by executing a constructor you will get the validation provided by that constructor. Along with that, it is possible to maintain (preferably supported by a lint) that casting into an extension type is bad style.
It's not going to be an absolute guarantee. For example, we could have an expression like e as X
where X
is a type variable whose value is an extension type at run time, and we can't in general determine statically which values any given type variable will have at run time.
However, constructor based validation of extension types is certainly as strong as having an isValid
method (in an extension type, a class, etc), because the language won't tell us if a specific object has never had any invocations of isValid
. If you manage to avoid casting to that extension type then it's actually a guarantee.
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:
- If you want an ironclad validation guarantee based on code in constructors then use a real wrapper class.
- If it's OK that validation relies on some programming conventions, and you want to avoid the time/space cost associated with a real wrapper object, use an extension type.
@mateusfccp wrote:
is there a way to validate when casting?
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 isValid
getter for any given extension type E
, and to maintain some conventions ensuring that isValid
is called whenever any object o
is accessed with static type E
and o
may be non-validated according to the reasoning behind those conventions.
The constructors of E
could invoke isValid
, or they could have an optimized version if the arguments of the constructor are already validated in some sense (e.g., an E.copy(E original)
constructor might rely on original
to be vetted already, so it might skip the validation entirely). In any case, if we maintain the convention that every constructor of E
will validate the representation object then the conventions about when to (re)check can be much simpler.
If one can simply cast into an extension type, it doesn't seem too useful for validations.
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 E
" and "promote to E
", and we have a lint against that, I'd claim that it is better than nothing.
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.
from language.
IIRC, you can always just cast into an extension type int as DegreesKelvin
? So you can always bypass the constructor entirely, making them not a great way of doing validation?
from language.
IIRC, you can always just cast into an extension type
int as DegreesKelvin
? So you can always bypass the constructor entirely, making them not a great way of doing validation?
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.
from language.
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 _DONT_CALL_ME
, and then a lint rule that fires any time it is invoked.
from language.
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.
from language.
The primary
keyword on a constructor in the body of the declaration which was mentioned in the original posting is already included in the proposal about primary constructors, here.
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');
}
}
from language.
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.
It's the way to declare the representation type and a name to access the representation object by, and then it also introduces a constructor.
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. extension type Foo._(Bar _bar) {...}
. Then it's almost as if it isn't there. (You can use Foo._DONT_CALL_ME
. The only one who can tell the difference between that an Foo._
is code inside your own library, but by all means make it longer if you want to.)
It doesn't change that anyone can do new Bar(args) as Foo
instead of Foo(new Bar(args))
.
Or even more indirect: [Bar(args1), Bar(args2)] as List<Foo>
or (Bar.new as Foo Function(Args))
.
from language.
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?
It doesn't change that anyone can do
new Bar(args) as Foo
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?
from language.
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 final
, which has no effect since it's always final:
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 enum
declaration. For example, it cannot be mutable, late or have an initializer, the format is 'final'? <type> <identifier>? ';'
.
If it has an identifier, that introduces a getter with that identifier as name, which can access the representation object (aka this
) at the representation type. Equivalent to int get foo => this as int;
, with the as int
being a no-op`.
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.
(We can allow, say, an explicit super(value)
in the initializer list, in case the representation value has no name, or an initializer of the form this = value
in the initializer list.)
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 new
or (the already allowed) const
:
extension type Foo(int) {
...
works like extension type Foo {int; }
above, and requires
extension type new Foo(int) {
...
to introduce a no-op constructor, const
instead of new
for a constant 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 o as Foo
and this as int
. You don't have to use them, and they are not doing anything that you can't do without them, but they're also pretty much cost-less.
(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 ._
constructor to forward to is useful when you create the representation object from different constructor arguments. Having a name for the representation object is almost always useful too.)
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 can live with the three extra characters of extension type VerySpecial._(Special _)
as a way to ask for not having a (public) constructor or representation variable, and if I squint just a little, it really isn't there.
from language.
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 can live with the three extra characters ofextension type VerySpecial._(Special _)
as a way to ask for not having a (public) constructor or representation variable, and if I squint just a little, it really isn't there.
I think I am coming to this conclusion as well; these cures all look worse than the disease to me 😄 .
from language.
@lrhn wrote:
it's a great reminder that vases are breakable, and a way for you to break it, should you really need to.
In some cases I might be pretty sure I don't want to break the vase, and I really don't need a reminder. ;-)
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.
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:
these cures all look worse than the disease to me
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.
from language.
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.
from language.
Related Issues (20)
- Extensions on nested generic mixins don't promote types properly HOT 6
- Disallow extension typed declarations with bottom as representation type - from implementing interface types. HOT 7
- Detect the following situation during compile time - not during runtime HOT 2
- [extension types] platform independence HOT 9
- static abstract members in interfaces HOT 2
- Map Patterns Deconstructing a Non-Existent Key Throws an Exception HOT 2
- Github action gets "This requires the 'macros' feature..." HOT 2
- Add `..` to continuation tokens for constructor-tearoff type argument selector.
- Allow extension type to implement its representation type. HOT 4
- const-by-default constructors HOT 5
- const_constructor_param_type_mismatch generates poor errors: HOT 2
- Change `augment super` to `augmented` (or similar) HOT 5
- literal-conforming assignment operator HOT 3
- Stop allowing void-to-void data transfer? HOT 10
- Clarify the kinds of instance members in specification documents HOT 1
- Make `augment` contextual in regular libraries and reserved in augmentation libraries HOT 3
- Make extension type non-nullability not be implicit. HOT 7
- Pattern matching an element in the middle of a list. HOT 5
- Change how `augmented` works for operators HOT 11
- Inconsistent behavior of field promotion in irrefutable patterns. HOT 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from language.