_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".
_Removed Type-Defect label._
_Added Type-Enhancement, Area-Language, Triaged labels._
_Set owner to @gbracha._
_Added Accepted label._
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.
ObjectPascal has virtual constructors which are basically what you'd need here.
Any update on this? Can we wait for this?
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).
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.
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?
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.)
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 withD.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.
With an expression of static type
Type<C>you cannot rely on it to have a methodstaticMethod
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.
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.
Most helpful comment
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.