Coder Social home page Coder Social logo

Comments (12)

eernstg avatar eernstg commented on June 26, 2024 1

@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.

jakemac53 avatar jakemac53 commented on June 26, 2024

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.

mateusfccp avatar mateusfccp commented on June 26, 2024

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.

srawlins avatar srawlins commented on June 26, 2024

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.

bwilkerson avatar bwilkerson commented on June 26, 2024

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.

eernstg avatar eernstg commented on June 26, 2024

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.

lrhn avatar lrhn commented on June 26, 2024

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.

eernstg avatar eernstg commented on June 26, 2024

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.

lrhn avatar lrhn commented on June 26, 2024

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.

srawlins avatar srawlins commented on June 26, 2024

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.

I think I am coming to this conclusion as well; these cures all look worse than the disease to me 😄 .

from language.

eernstg avatar eernstg commented on June 26, 2024

@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.

eernstg avatar eernstg commented on June 26, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.