Skip to content
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

Make classes first-class citizens #10667

Open
DartBot opened this issue May 15, 2013 · 12 comments
Open

Make classes first-class citizens #10667

DartBot opened this issue May 15, 2013 · 12 comments
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). type-enhancement A request for a change that isn't a bug

Comments

@DartBot
Copy link

DartBot commented May 15, 2013

This issue was originally filed by tloeffle...@gmail.com


It would be nice if classes could be passed around as objects the same way closures/functions are, e.g.:

void foo() => print("bar");
var baz = foo;
baz();

This works fine. For classes, however, this

class Foo {
}
var Bar = Foo;
var baz = new Bar();

fails with "using 'Bar' in this context is invalid".

@lrhn
Copy link
Member

lrhn commented May 15, 2013

Removed Type-Defect label.
Added Type-Enhancement, Area-Language, Triaged labels.

@gbracha
Copy link
Contributor

gbracha commented Aug 25, 2014

Set owner to @gbracha.
Added Accepted label.

@gbracha
Copy link
Contributor

gbracha commented Jan 7, 2015

To a degree, classes are first class (you can pass Type objects around) but we require literal types in new. There are reasons for that: types do not and should not describe the signatures of constructors (i.e., if you implement an interface, you don't want to be forced to implement the same constructors).

Languages that allow this sort of thing don't have constructors in the C++/Java tradition. Instead, they have instance methods on the class objects. In Dart, we could assume that every type object has instance methods that correspond to its constructors and static methods, and you can use those instead of new.

We could do that (and I am in favor) but we have not made any decision.

@Hixie
Copy link
Contributor

Hixie commented Jan 8, 2015

ObjectPascal has virtual constructors which are basically what you'd need here.

@DartBot DartBot added Type-Enhancement area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). labels Jan 8, 2015
@kevmoo kevmoo added P2 A bug or feature request we're likely to work on type-enhancement A request for a change that isn't a bug and removed accepted labels Feb 29, 2016
@amsakanna
Copy link

Any update on this? Can we wait for this?

@eernstg
Copy link
Member

eernstg commented Apr 8, 2019

All traces of the metaclass feature mentioned here ('they have instance methods on the class objects') have been removed from the language specification over time, mainly because that feature is inherently at odds with static typing. For example:

class C {
  static void staticMethod() {}
  C.named();
}

main() {
  dynamic d = C(); // We've now forgotten that this is a `C`.
  Type t = d.runtimeType; // Get hold of the reified type of `d`.

  // The reified type would then have an instance method for each "class method" of `C`.
  t.staticMethod(); // Calls `C.staticMethod()`.
  dynamic other = t.named(); // Creates an instance like `C.named()`.
}

Let's call the static methods and constructors of a class C its static interface.

The static interfaces are islands in the type system, in the sense that there is no subtype relationship between the static interface of a given class C and that of any other class D, and there is also no relationship to the regular interface of any class (i.e., the interface which is concerned with its instance members). So we would have to introduce the notion of a static interface (maybe denoted by C.static or something like that), and then we would just have a large number of unrelated static interfaces, and not even a typed context could give us any static guarantees:

class C ... // Same as before.
class D implements C { // Or `extends`, that does not matter.
  static int completelyDifferentStaticMethod(double d) => 42;
  D.otherName();
}

main() {
  C c = D.otherName(); // We don't know statically that `c` is a `D`.
  if (...) c = C.named(); // .. and, in general, we can't know such things.
  Type<C> t = c.runtimeType; // Assuming a generic `Type`.
  t.staticMethod(); // Works for `C`, not for `D`. Hence: Not safe.
}

We could change Type to be a generic class, and maintain the invariant that the reified Type for a type T has type Type<T> (so t is Type<C> when t is the reification of C or D, but t is Type<D> is false when t is the reification of C). This would make a number of things more statically safe for code that uses Type. We could even introduce a special rule saying that Type<T> would have type T.static when T denotes a class (which could have the form G<T1..Tk> where G is some generic class).

However, that wouldn't even help us here, because the static interface of D is completely isolated from the static interface of C, there is no subtype relationship nor any other relationship, they are just different.

So the metaclass concept certainly does not fit well into a statically typed setting, unless we change the requirements on static interfaces radically.

But it would be massively, massively breaking to start forcing the static interface of all classes to be a subtype of that of its superinterfaces. It probably wouldn't work well in practice either, because it's just not obvious that you want your class D to have its own overriding declaration of a constructor D.m for every m where one of the superinterfaces of D has a constructor with the name m.

Further discussion on this topic could go in some other direction, but I believe it's a safe bet that Dart will not have metaclasses (in the sense that Type instances have instance methods corresponding to the static interface of the reified class).

@Hixie
Copy link
Contributor

Hixie commented Apr 10, 2019

It would certainly be breaking, but I don't think it need be massively breaking.
In particular, statics could be defined as non virtual by default.

@eernstg
Copy link
Member

eernstg commented Apr 10, 2019

We could allow an instance t of Type<D> to invoke C.staticMethod() as t.staticMethod() even though there is no static method named staticMethod in D:

class C {
  static staticMethod() {}
  C(int i);
}

class D implements C {
  D.name(double d, double d);
}

main() {
  Type<C> t = D;
  t.staticMethod(); // Calls `C.staticMethod()`.
}

This basically means that every type will inherit all the static methods of all its supertypes. There will probably be some name clashes to sort out, but that might work.

The difficult part, I suppose, would be to ensure that every type has all the constructor signatures of all its supertypes. So if we have the constructors shown above then we must be able to construct a D using an int:

main() {
  Type<C> t = D;
  C c = t(42); // We do have a constructor `C(int)`, but we must create a `D`.
}

At least, it seems wrong to me if a supposed constructor invocation on a value of type Type<T> for some T would create an instance of an arbitrary supertype of T (whoever has a constructor with the right parameter list). It also seems pretty work-intensive to me (and inconvenient) to write enough constructors such that we actually do have a way to create a D from an int, etc.etc. (for all classes in the whole world).

If you don't have that, how would you ensure that metaclass construction is type safe?

@Hixie
Copy link
Contributor

Hixie commented Apr 12, 2019

In your description, everything is assumed to be virtual. I'm saying, we don't have to assume that.

t.staticMethod() in your first example could fail with D.staticMethod is not defined.

t(42) in your second example could fail with C constructor is not virtual, so cannot be referenced here.

That gets us to where we are today.

Then you could opt-in to exposing these static methods and constructors:

class C {
  virtual C(int i) { print('C($i)'); }
  virtual C.name(String s) { print('C.name($s)'); }
  virtual static staticMethod() { print('C.staticMethod()'); }
  virtual static staticMethod2() { print('C.staticMethod2()'); }
}

class D extends C {
  D.name(String s) : super(s) { print('D.name($s)'); }
  static staticMethod2() { super.staticMethod2(); print('D.staticMethod2()'); }
}

class E implements C {
  E(int i) { print('E($i)'); }
  D.name(String s) : { print('E.name($s)'); }
  // compiler error: E does not implement C.staticMethod()
  static staticMethod2() { print('E.staticMethod2()'); }
}

main() {
  Type<C> t = D;
  t.staticMethod(); // prints "C.staticMethod()"
  t.staticMethod2(); // prints "C.staticMethod2()" then "D.staticMethod2()"
  print(t(1).runtimeType); // prints "C(1)" then "D"
  print(t.name('x').runtimeType); // prints "C(x)" then "D(x)" then "D"
  t = E;
  t.staticMethod2(); // prints "E.staticMethod2()"
  print(t(1).runtimeType); // prints "E(1)" then "E"
  print(t.name('x').runtimeType); // prints "E(x)" then "E"
}

Or if you want to get really fancy:

class C {
  virtual static staticMethod() { printMe(); }
  virtual static printMe() { print('I am C'); }
}

class D extends C {
  static printMe() { print('I am D'); }
}

main() {
  C.staticMethod(); // prints "I am C"
  D.staticMethod(); // prints "I am D"
  Type<C> t = D;
  t.staticMethod(); // prints "I am D"
}

You could also have virtual static getters, setters, and fields.

(Side note, I wish we had a This variable, similar to this, which was a Type variable whose value was the value of the current class. It'd be useful in generics, and it would be useful in code like the above, where in a virtual static you may wish to be able to refer to the current type explicitly.)

@eernstg
Copy link
Member

eernstg commented Apr 14, 2019

It's an interesting topic! I think the pieces could fit together with a radical model (if we have "everything" at the meta-level), but it will probably not be easy to achieve, for instance, type safety if we have less than that.

@Hixie wrote:

everything is assumed to be virtual

Actually, I'm just assuming that if T <: S and S is an interface type that has a method m then T also has a method m. The signatures of the two ms would be somehow related (e.g., T.m would have to be a correct override of S.m in basically all typed OO languages; in Dart this includes the ability for a parameter to be covariant, but otherwise it basically means that T.m <: S.m).

In other words, we could have support for both virtual (that is: normal) static methods and non-virtual static methods (that is, methods that can not be overridden), but all of them should at least be inherited.

I think the most reasonable and powerful approach would be to say that we are talking about instance methods on class objects, that is, methods declared in metaclasses. This then means that static methods and constructors are regular methods on a class object (i.e., on an instance of a metaclass), which again means that all the normal rules should apply at the meta-level. This would give us a static analysis and a semantics for invocations, for tear-offs, for superinvocations, for "everything", which would otherwise be a long list of newly invented rules.

t.staticMethod() in your first example could fail with D.staticMethod is not defined.

I think this implies that Type<D> wouldn't inherit staticMethod from its supertype Type<C>.

I do think that any developer who has experience with Dart or any other typed OO language would be justified in expecting inheritance to be supported, even for methods that can not be overridden. So it's at best surprising.

Apart from that, of course, it's not type safe: With an expression of static type Type<C> you cannot rely on it to have a method staticMethod, because that's only true for Type<C>, but it may not be true for any of its subtypes, such as Type<D>. That's what I meant when I mentioned that it would be hard to make this kind of feature type safe.

@Hixie
Copy link
Contributor

Hixie commented Apr 22, 2019

With an expression of static type Type<C> you cannot rely on it to have a method staticMethod

That's already true:

  C.staticMethod(); // works
  var x = C;
  x.staticMethod(); // fails

I'm just saying that we should continue doing that, but add the concept of inherited/virtual methods to these metaclasses.

do think that any developer who has experience with Dart or any other typed OO language would be justified in expecting inheritance to be supported

It's supported, just has to be explicitly opted-into.

An alternative approach would be to use the static type for dispatch to non-virtual static methods:

class C {
  static staticMethod() { print('C.staticMethod()'); }
  virtual static virtualStaticMethod() { print('C.virtualStaticMethod()'); }
}

class D extends C {
  static staticMethod() { print('D.staticMethod()'); }
  static virtualStaticMethod() { print('D.virtualStaticMethod()'); }
}

class E implements C {
  static virtualStaticMethod() { print('E.virtualStaticMethod()'); }
}

main() {
  C.staticMethod(); // prints "C.staticMethod()"
  D.staticMethod(); // prints "D.staticMethod()"
  // there is no E.staticMethod()
  Type<C> t = D;
  t.staticMethod(); // prints "C.staticMethod()" - note, NOT D.staticMethod()
  t.virtualStaticMethod(); // prints "D.virtualStaticMethod()"
  t = E;
  t.staticMethod(); // prints "C.staticMethod()"
  t.virtualStaticMethod(); // prints "E.virtualStaticMethod()"
}

That would be consistent with e.g. what ObjectPascal does. I'm not sure I can think of another language that has static typing and metaclasses.

@eernstg
Copy link
Member

eernstg commented May 21, 2019

Cf. a related issue in the language repo: dart-lang/language#356, including this comment, which goes deeper into a potentially useful emulation of virtual static methods: Use a companion object to the class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests

7 participants