-
Notifications
You must be signed in to change notification settings - Fork 212
More capable Type
objects
#4200
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
Comments
That sounds like a Kotlin companion object.
Can we have class C {
// Before any static member:
static class extends A.class with HelperMixin1 implements HelperType2;
// Following static members are membes of this static class.
} and you can access the type as You can only extend or implement a
We probably still want to ensure that tear-offs from a static-member type is a canonicalized constant. Somehow. (That's one of the reasons I don't want normal classes to be able to implement the special singleton classes.) To make this equivalent to the current behavior, I assume constructors have access to the type parameters of the (Also, it's currently possible to have
Rather than needing this intersection, just let the generated mixin be
Probably want both a non-static and static type bound, say This will be yet another reason to allow static and instance members with the same name. How will this interact with So, dynamic x;
// ...
... (Object? o) {
...
x = o.runtimeType;
...
...
x.foo(); we may have to retain a lot of static methods that could be tree-shaken today, because we can't tell statically whether they're invoked or not. |
print((int).runtimeType); // _Type
print((int).runtimeType.runtimeType); // _Type
print((String).runtimeType); // _Type But... if (I think, the explanation is that Maybe a function like |
I would say that Then Or maybe that's a bad idea, because of what it does to tree shaking. That is:
A generic type's class objects have instances for each instantiation, and the constructor members can access those. The getters and setters of static variables are not represented by instance variables, they all access the same global variable's state. |
Great comments and questions, @lrhn!
It's similar to Kotlin (and Scala) companion objects, but also different in other ways: I'm not suggesting that Dart classes should stop having the static members and constructors that we know today, I'm just proposing that we should allow developers to request access to some of the static members and constructors (determined by the interface of the This differs from the companion objects in that there is no support for obtaining a companion object from a given actual type argument (because the type argument was erased). In contrast, a Dart type argument will always be able to deliver the corresponding For example: abstract class A<X> { X call(); }
class B1 static implements A<B1> {}
class B2 {}
void main() {
var type = someExpression ? B1 : B2;
if (type is A) {
var newObject = type();
}
} This means that the Kotlin/Scala developer must predict the need to invoke static members or constructors up front (when the concrete type is known) and must pass a reference to the required companion object(s) along with the base data. In contrast, Dart programs can pass type arguments along in the same way they do today, and then it will be possible to get access to the static members and constructors arbitrarily late, as long as the given type is available as the value of a type parameter. In the case where a class does not have a
This seems to imply that we would implicitly generate a static member or a constructor from the instance method implementations. This may or may not be doable, but I'm not quite sure what the desired semantics should be. When it comes to code reuse I would prefer to have good support for forwarding, possibly including some abstraction mechanisms (such that we could introduce sets of forwarding methods rather than specifying them one by one). This could then be used to populate the actual class with actual static members as needed, such that it is able to have the desired
I think the current approach to constants based on constructors and static members is working quite well. The ability to access static members and constructors indirectly via a reified type is an inherently dynamic feature, and I don't think it's useful to try to shoehorn it into a shape where it can yield constant expressions. There's no point in abstracting over something that you already know at compile time anyway.
Good point! Done.
I'm not quite sure what In that case,
A class that doesn't have a A class that does have a static implements clause makes some of its static members and constructors callable from the implicitly generated forwarding instance members, but this is no worse than the following: class A {
static int get foo => 1;
}
class B {
void foo() {
print(A.foo);
}
} We may still be able to detect that With "more capable It may be harder, but it does sound like a task whose complexity is similar to that of tracking invocations of instance members. That is, if we're able to determine that |
@tatumizer wrote:
If you evaluate a type literal (such as In particular, it is certainly possible for an implementation to evaluate Those reified objects may then have different interfaces, that is, they support invocations of different sets of members, so certainly it's possible for On the other hand, the reified There's nothing special about this (and that's basically the point: I want this mechanism to use all the well-known OO mechanisms to provide flexible/abstract access to the static members and constructors which are otherwise oddly inflexible, from an OO perspective).
If we have So we can certainly do abstract interface class MyInterface {
int get foo;
}
class D static implements MyInterface {
static int get foo => 1;
}
void f<X extends D>() {
var reifiedX = X;
if (reifiedX is MyInterface) {
print(reifiedX.foo);
}
} |
Could the following code similar to C#'s syntax: abstract class Json {
static fromJson(Map<String, dynamic> json);
Map<String, dynamic> toJson();
}
class User implements Json { ... } be syntatic sugar for this proposal's syntax? abstract class Json$1 {
Map<String, dynamic> toJson();
}
abstract class Json$2 {
fromJson(Map<String, dynamic> json);
}
class User implements Json$1 static implements Json$2 { ... } To keep EDIT: I can't imagine any piece of code implementing the same interface for instance methods and static methods. However, I can imagine most use cases implementing an interface with instance methods and static methods. That's why I would prefer if instance and static interfaces were merged. |
@eernstg wrote:
This can't be! Today (An argument against |
The That doesn't mean that (I chose Spoiler: void main() {
static(C).static(C.static.static.C).static(C).static;
}
class C {
static C get static => C();
C call(Object? value) => this;
}
C static(Type value) => C();
typedef _C = C;
extension on _C {
_C get static => this;
_C get C => this;
} |
Just to clarify: speaking of Object methods, I didn't mean
That's not the reason to disqualify the word - in practice, it won't hurt anyone. Most people believe Still, it's not clear what |
Class |
If A is a regular class, we can always say Q: Can class say |
Kotlin's companion object model is worth looking into, it won't take long: https://kotlinlang.org/docs/object-declarations.html#companion-objects Main difference is that the companion object has a name and a type, and - most importantly - Kotlin has a word for it, which is (predictably) "companion object". It would be very difficult to even talk about this concept without the word, so I will use it below. The idea is that you can extract a companion object from the object, e.g., (using dart's syntax), I don't know if there's a simple way to express the same concept in dart without introducing the term "companion object". Does anyone have issues with this wording? Interestingly, you very rarely need to extract the companion object explicitly, but the very possibility of doing so explains a lot: it's a real object; consequently, it has a type; this type can extend or implement other types - the whole mental model revolves around the term "companion object". |
@Wdestroier wrote:
abstract class Json {
static fromJson(Map<String, dynamic> json);
Map<String, dynamic> toJson();
}
class User implements Json { ... } Desugared: abstract class Json$1 {
Map<String, dynamic> toJson();
}
abstract class Json$2 {
fromJson(Map<String, dynamic> json);
}
class User implements Json$1 static implements Json$2 { ... } I agree with @mateusfccp that it is going to break the world if a clause like In other words, it's crucial for this proposal that abstract class StaticInterface1 { int get foo; }
abstract class StaticInterface2 { void bar(); }
class B static implements StaticInterface1 {
final int i;
B(this.i);
static int get foo => 10;
}
class C extends B static implements StaticInterface2 {
C(super.i);
static void bar() {}
} This also implies that there is no reason to assume that a type variable void f<X extends Y, Y static extends StaticInterface1>() {
Y.foo; // OK.
X.foo; // Compile-time error, no such member.
} |
I must say I am in love with this proposal. It solves many "problems" at once (although it may introduce more? let's see how the discussion goes), and it's a very interesting concept. I agree that it's not as straightforward to understand it, but once you understand, it makes a lot of sense. I also understand the appeal in having the dynamic and static interfaces bundled together, as @Wdestroier suggests, so if we could come with a non-breaking and viable syntax for doing it (while still providing the base mechanism), I think it would be valuable. |
@tatumizer wrote:
Today If you want to test whether the reified type object for void main() {
var ReifiedString = String; // Obtain the reified type object for the type `String`.
if (ReifiedString is Object) { // Same operation as `String is Object`.
// Here we know that `ReifiedString` is an `Object`, but
// we knew that already, so we can't do anything extra.
ReifiedString.toString(); // Can do, not exciting. ;-)
}
} Testing that It's the instance members of the tested type This means that if The reason why I'm emphasizing that it's all about instance members is that we already have the entire machinery for instance members: Late binding, correct overriding, the works. The static members and the constructors are just used to derive a corresponding instance member that forwards to them, and all the rest occurs in normal object land, with normal OO semantics. |
@eernstg : I understand each of these points, but they don't self-assemble in my head to result in a sense of understanding of the whole. The problem is terminological in nature. See my previous post about Kotlin. |
About Kotlin's companion object, I already mentioned it here. The main point is that you cannot obtain the companion object based on a type argument, you have to denote the concrete class. This reduces the expressive power considerably, because you can pass the type along as a type parameter and you can pass the companion object on as a regular value object, and then you can use the type as a type and the companion object as a way to access static members and constructors of that type. This is tedious because you have to pass the type and the object along your entire call chain whenever you want to use both. You could also pass the type alone, but then you can't get hold of the companion object. Or you could pass the companion object alone, but then you can't use the type (which is highly relevant, e.g., if you're calling constructors). I spelled out how it is possible to write a companion object manually in Dart in this comment. With this proposal, we can obtain the "companion object" (that is, the reified type object) for any given type (type variable or otherwise) by evaluating that type as an expression at any time we want, and we can test whether it satisfies a given interface such that we can call the static members / constructors of that type safely. |
@tatumizer wrote:
The role of the companion object in Kotlin and Scala is played by the reified type object in this proposal. We could of course also introduce an indirection and just add a |
@eernstg : that's where I have to disagree. It's an extra step for cognitive reason, which is a hell of a reason. :-) (Main property of a companion object, apart of its very existence, is that it has a clear type, which is visibly distinct from the type of the object itself, and it's a part of an (explicitly) different hierarchy. The difference is in explicitness). |
If we just use the phrase 'companion object' rather than 'reified type object', would that suffice? I don't think there's any real difference between the members of a companion object in Scala and Kotlin, and the forwarding instance members in this proposal, so the reified type object is the companion object. There is a difference in that this proposal only populates the companion object with forwarding instance members when the target class has a We could also choose to populate every reified type object with all static members and all constructors. However, I suspect that it's a better trade-off to avoid generating so many forwarding methods because (1) they will consume resources (time and space) at least during compilation, and perhaps the unused ones won't be fully tree-shaken, and (2) they aren't recognized by the static type system if the reified type object doesn't have any useful denotable supertypes (so all invocations would then have to be performed dynamically).
Good point! It is indeed an element of implicitness that this proposal works in terms of a 1:1 connection between each targeted static member / constructor and a forwarding instance member of the reified type object. We don't have that kind of relationship anywhere else in the language. The connection is created by specifying exactly how we will take a static member declaration and derive the corresponding instance member signature, and similarly for each constructor. I don't know how this could be made more explicit without asking developers to write a lot of code that would be computed deterministically anyway (which is basically the definition of redundancy). For the type, the |
I don't know if that will suffice, but it would certainly be a step in the right direction. When I see the expression "reified type object", my head refuses to operate further - though I can guess what this means, I'm not sure the guess is correct, and the word "reified" especially scares me off (like, WTF: how this reified type object differs from just type object?). I'll respond to the rest of your comments here later, need time to process :-). |
True, it's important to be an opt-in feature. abstract class MyClass {
// No effect on subtypes.
static String name() => 'MyClass';
// Has effect on subtypes.
abstract MyClass();
// Has effect on subtypes.
abstract static MyClass fromMap(Map<String, dynamic> json) =>
MyConcreteClass(property: json['property']);
} |
Here's an arrangement I could understand:
print(String.companionObject is StringCompanion); // true
print(String.companionObject.runtimeType == StringCompanion); // true This is achieved by adding one method to the Type class: class Type {
Object companionObject; // just an Object
// etc...
}
class Foo<T static implements SomeKnownInterface> {
bar() {
T.companionObject.methodFromKnownInterface(...);
}
}
class Foo static implements FromJson<Foo> {
// no changes to the existing syntax
static Foo fromJson(String str) { ... }
} The companion object will only include the methods from the
The difference of this design and the original one is that, given a type parameter T, you can write WDYT? (Possible alternative for "companionObject": "staticInterfaceObject" or something that contains the word "static") |
Very good! I think I can add a couple of comments here and there to explain why this is just "the same thing with one extra step" compared to my proposal. You may prefer to have that extra step for whatever reason, but I don't think it contributes any extra affordances. Also, I'll try to demystify the notion of a 'reified type object'.
Right, that's exactly what I meant by 'We could of course also introduce an indirection' here.
You'd have to use print((String).companionObject is StringCompanion); // true
print((String).companionObject.runtimeType == StringCompanion); // true But the value of evaluating a type literal like The core idea in this proposal (meaning "the proposal in the first post of this issue") is that this object should (1) have instance members forwarding to the static members and constructors of the type which is being reified, and it should (2) have a type that allows us to call those static members and constructors (indirectly via forwarders) in a type safe manner, without knowing statically that it is exactly the static members and constructors of Returning to To compare, this proposal will do exactly the same thing in the following way (assuming that print(String is Interface); // true
print((String).runtimeType == Interface); // false The second query is false because the run-time type is not exactly
Here's how to do it in this proposal: class Foo<T static extends SomeKnownInterface> {
bar() {
(T).methodFromKnownInterface(...);
}
} The proposal has an extra shortcut: If we encounter
These statements are true for this proposal as well.
The corresponding statement for this proposal is that if the class doesn't declare
In your proposal you can write I hope this illustrates that the two approaches correspond to each other very precisely, and the only difference is that the |
@eernstg: I think I can pinpoint the single place where our views diverge, and it's this: |
Great, I think we're converging!
That's generally not a problem. In my proposal, the RTO for a given class has a type which is a subtype of Currently, we already have a situation where the result returned from the built-in This implies that it isn't a breaking change to make those evaluations yield a result whose type isn't Next step, the static type of an expression that evaluates a type literal that denotes a class/mixin/etc. declaration can include the meta-member mixin. In other words, the static type of This implies that we can safely assign this RTO to a variable with the abstract class StaticFooable { void foo(); }
class A static implements StaticFooable {
static void foo() => print('A.foo');
}
void main() {
StaticFooable companion = A;
companion.foo(); // Prints 'A.foo'.
} However, we can not use an overriding declaration of This means that we don't know anything more specific than It might seem nice and natural if we could make the static interface co-vary with the interface of the base object itself (such that we could use However, note that it would certainly be possible for a type argument to use subsumption in static interface types: abstract class StaticFooable { void foo(); }
abstract class StaticFooBarable implements StaticFooable { void bar(); }
class B extends A static implements StaticFooBarable {
static void foo() => print('B.foo');
static void bar() => print('B.bar');
}
void baz<X static extends StaticFooable>() {
X.foo(); // OK
X.bar(); // Compile-time error, no such member.
(X as dynamic). bar(); // Succeeds when `X` is `B`.
}
void main() {
baz<B>(); // OK, `StaticFooBarable <: StaticFooable` implies that `B is StaticFooable`.
} |
Why do you need this Compare with this: we can declare some method as returning To be sure if we are on the same page, please answer this question. The static type of expression (After re-reading your response, I am not even sure we disagree on anything important, And I do acknowledge the benefits of your proposal, assuming we agree that |
After considering it more, the idea of including in RTO only the references to methods from the declared static interfaces might be unnecessary. Whenever the tree-shaker decides to preserve class C, it will most likely have to preserve its noname constructor, too (otherwise, no one can create an instance). Apart from constructors, the classes rarely have static methods, and those that are potentially used could be identified by name (e.g. if someone says Another point: I think static interfaces don't make much sense. I can't imagine the situation where a class expects type parameter to implement some static methods without also expecting the instances to implement some specific regular methods. abstract class Ring<T> {
abstract T operator+(T other);
abstract T operator*(T other);
// ...etc
abstract static T zero; // SIC!
abstract static T unity;
}
class Integer implements Ring<Integer> {
final int _n;
Integer(this._n);
// ...implementation of operators,
static const zero = Integer(0);
static const unity = Integer(1);
} |
Reordered response:
Do you mean 'what can you do with the reified type objects'? The type variables do not have any new affordances, but if you evaluate a type variable as an expression then you'll obtain a reified type object. As always, they have type However, with this proposal you'll get an instance whose run-time type is a subtype of abstract class Fooable {
void foo();
}
class A1 static implements Fooable { // OK.
static void foo() {}
}
class A2 static implements Fooable { // Error!
static void foo(int i) {} // Cannot be used to implement `Fooable.foo`.
}
No, a reified type object can not be used as a type, they are not the same kind of thing.
Exactly (assuming that there is an extension somewhere that adds this capability to
Not true. We still have properties like
Yes, we're implicitly inducing a class which is a subtype of
Extension methods are statically resolved. That is, it is known at compile time exactly which portion of code is being executed when we call an extension method. The whole point with 'more capable type objects' is that they allow us to invoke static members and constructors without the need to know statically exactly which portion of code you will be running. In other words "what stops us" is the simple fact that we're aiming to do the exact opposite of what extensions do. But the following is very far from being regular extensions:
Trying to understand what that would mean, it looks like we're declaring an extension that adds an extension instance method Next, I'd assume that abstract class Summable<T> {
T sum();
} So you're saying, IIUC, that we could introduce a feature whereby the compiler/analyzer would implicitly induce a class declaration which is a subtype of We'll need a receiver (the object denoted by // Implicitly induced, based on the extension shown above.
class SummableInt_for_ListInt {
List<int> list;
SummableInt_for_ListInt(this.list);
int sum() => list.sum();
} But this means that However, I don't think there is much of a relationship with this issue, or with the
When we're reifying a type there is no original object (a type is not an object). We're just using a mechanism that provides support for obtaining an object (a reified type object) from a given type. We've had such a mechanism since the very beginning of the language Dart. What I'm proposing here is that we should generalize this mechanism such that (1) the reified object which is obtained directly from the type itself (that is, the one which is based on the declaration of that type) can provide access to static members and/or constructors of that declaration, and (2) extensions can support additional ways to reify the type to RTOs that implement/extend specific types. The former is the basic mechanism which can be used whenever you "own" all the types that you wish to use in a particular way. The latter allows us to add support for whichever feature you want to specific types that you can't edit. (For example, we can make
You'd need to specify a very special semantics for Next, with I think I'll stick to the syntax I've proposed. |
I'll assume it means something like the following: // I'm assuming the following declaration of `Deserializable`:
abstract class Deserializable<Self> {
Self readFrom(Reader r);
}
extension on String {
static implements Deserializable<String>;
static String readFrom(Reader r) => r.readString();
} In terms of the proposal in this issue, this is a compile-time error: The static member However, if we assume that we've added static extensions to the language then the declaration of With that, it should be a reasonable small generalization of the mechanism that implicitly induces a class that it can create an implementation that forwards invocations of So if we assume that static extensions are added to the language then it's reasonable to say that there is nothing wrong. If we're only assuming that the feature proposed in this issue is added then we need |
In the search for a way to create a suitable RTO, the declaration of Otherwise, we consider all extensions in scope whose on-type can be matched by Among those If this yields exactly one If it doesn't yield any, or if it yields several such However, note that when the 2nd type argument passed to However, we can also allow the 2nd type argument to
No,
It finds the relevant extension and the relevant
No, that wouldn't work. We can't make a class implement-or-extend
They both satisfy
I'm not 100% sure I can recall what that was about. However, I think they are completely different things:
|
OK, @tatumizer, that was a long rant. Hope it helps! ;-) |
Thanks! It a progress :-) Let's stick to your syntax.
class SummableInt_for_ListInt {
List<int> _list;
SummableInt_for_ListInt(List<int> list): _list=list;
int sum() => _list.sum();
} EDIT: Upon reflection, this doesn't make much sense. To be revisited later. Let's forget about union types for now. There is a connection to union types, but it's a connection of a different kind - let's talk about it later.
I see. My mental model was wrong: I thought type object directly contains references to base class and interfaces, but (I'm guessing) it contains a class id, and there's a table somewhere that maps class id of every class to class ids of the base class and class ids of the interfaces. Logically, it's the same, but with a twist. And I understand now why you have to generate a class that extends I've been working on the etude of my own. Write a function I have no idea how to write it. Belatedly responding to your earlier comment:
I don't think it matters much. The implementation of deserialization for any Footnotes
|
In principle, we can specify any arbitrary semantics we want for every term which is syntactically recognizable as such. For example, we can define that However, I'd like to keep the "magic" at the lowest possible level. One reason for this is that I want to avoid introducing large amounts of special casing in the semantics of the language: That's harder to understand and internalize as "natural", and it gives rise to more special casing code in the analyzer and compilers. In particular, it's crucial that class MyType implements Type {} // Sure, we can do that! In other words, there is no guarantee that every object of type So we definitely don't want to call Next, we can't just specify
I think that's true, but we don't promise anything before we're basically ready to release a feature. It's is always possible that something causes the evolution of the language to take a turn in an unexpected direction.
It's an RTO for
My proposal says that it is The static type of an evaluation of a type literal as an expression can be more precise and include both supertypes. This means that the implicitly induced class corresponding to a
// The following class is generated by the compiler/analyzer.
class MyReifiedType extends Type implements Deserializable<String> {
String readFrom(Reader r) {...}
} What was the problem? About For now, we can just continue to talk about
You can test
Rigth, you cannot say that because
Well, I'm thinking "What's the problem?" ;-)
If void f(Type rto) {
print(rto);
if (rto is! CallWithTypeParameters) {
// Could be a generic type without support, but we just assume we're done.
return;
}
if (rto.numberOfTypeParameters != 1) throw IllegalArgumentException("...");
final nextRto = rto.callWithTypeParameter(
1,
<X>() => Type.reify<X, CallWithTypeParameters>())
);
if (nextRto == null) throw IllegalArgumentException("...");
f(nextRto);
} You can't do it in current Dart, but if you enhance the capabilities of RTOs as in this proposal then it is possible. About static type checking and deserializing a
That's a good point. With the feature which is proposed here, we're talking about including (By the way, how would you manage to add In the situation where we can equip every class that participates in a particular activity with the required behaviors (e.g., enabling deserialization with every class which is part of a deserialization step), then there is no need to have less complete type checking: Each class So if we're in a position where we can add support for However, in the case where we wish to include support for some classes that we can't edit (e.g., that's quite likely to be true for In this case we can't require I do think, though, that the added expressive power of the type-abstracting approach based on 'more capable type objects' (that is, avoiding a lot of code duplication, and simply handling more cases) will be a very meaningful choice in many cases where we must handle some classes "on the side" because we can't directly edit them. If we can just opt in all the classes that are participating then there is no trade-off, as I mentioned, it's just as type safe as the rest of Dart. |
You can declare any problem a non-problem by providing an (imaginary) non-solution. :-) How does the RTO in question becomes equal to String ( class MyReifiedType extends Type implements Deserializable<String> {
String readFrom(Reader r) {...}
} Where does it say it's a subtype of EDIT: rather than creating a new class like
I don't have to. For a code generator, Here's my version of the etude: f(Type T) {
print(T);
if (T case Type<List<var X>>) f(X);
}
// instance version
g<T>(T value) {
print('type=$T value=$value');
if (T case Type<List<var X>>) g(value.first as X);
}
main() {
f(List<List<List<int>>>);
g([[[1]]]);
} Here's the connection to union types: we discussed the serialization of a recursive Serializable type expressed as a union type. It would be highly surprising if for deserialization we would have to use a totally different mechnism. Note that with a naive generator, these two are perfectly symmetric. Same symmetry would be expected here. Maybe this observation can give us a clue? This begs the question: what is the minimal set of features that allows us to implement seralization/deserialization using a generator, with no redundant code?
All other features are unnecessary for that purpose - specifically:
Agree? :-) |
class A {
final int i;
A(this.i);
bool operator ==(Object other) => other is A && i == other.i;
int get hashCode => i.hashCode;
}
class B extends A {
B(super.i);
}
void main() {
print(B(15) == A(15)); // 'true', no problem.
} A reified type object as an object whose run-time type is a subtype of In this example, the instance variable
We have reified type objects that are reifying various types, but they don't have to have a type that has any direct relationship to the type that they reify. For example, it's perfectly possible for an implementation to reify all types as instances of a specific class (say, With this feature, they would not be instances of
No, the RTO
It is It is still very important, of course, to allow developers who wish to stick to a more restricted usage to determine at compile time that the lookup is guaranteed to succeed.
It isn't a subtype of The name It would be nice to find a way to preserve the Another idea which has been mentioned would be that the RTO for a type
That wouldn't work when the capability is added by an
You're assuming that we have an existential open operation. One of the really powerful things about this proposal is that we can create support for a reasonable replacement for an existential open operation ( The important thing to be aware of here is that the existential open mechanism would be a substantially bigger effort. Also, an existential open mechanism won't help us if the goal is to be able to invoke some static members and/or constructors without being forced to write monomorphic code (where all types are resolved at compile time): An existential open mechanism can only do something if you're working with a generic type, but one (very basic) scenario for the invocation of static members involves non-generic classes. There's no way you can use an existential open operation to help you abstract over an invocation of two static members You might think we could obtain the RTO for Yes, I know, this is only type safe if we have |
(Continuing a correspondence chess game. Here's my next move) Before going forward, let's check the position on the board: var t = Type.reify(String, Deserializable<String>);
// static type of t is "Deserializable<String>?", period! Not anything "more specific".
if (t != null) {
t == String; // true
t is Type; // true
t is Deserializable; // true
t is Deserializable<String>; // true
} Here's a copy of your earlier program void f(Type rto) {
print(rto);
if (rto is! CallWithTypeParameters) {
return;
}
if (rto.numberOfTypeParameters != 1) throw IllegalArgumentException("...");
final nextRto = rto.callWithTypeParameter(
1,
<X>() => Type.reify<X, CallWithTypeParameters>())
);
if (nextRto == null) throw IllegalArgumentException("...");
f(nextRto);
} This program reminds me of Zones. (No need to say more :-)). Here's another version that works without parametrized Types or existential open: void f(Type rto) {
print(rto);
if (Type.isSubtypeOf(rto, List)) {
f(Type.typeArguments(rto)[0]);
}
} Another (better?) syntax for the same idea would be void f(Type rto) {
print(rto);
if (Type(rto).isSubtypeOf(List)) {
f(Type(rto).typeArguments[0]);
}
} This resembles the syntax we use for disambiguation while working with named extensions like As soon as we introduce extension on List<int> static implements Bar {
static void helloFromBar(); // the method from Bar interface
}
Bar bar = Type.reify(List<int>, Bar);
bar.helloFromBar();
if (Type.isSubtypeOf(bar, List<int>) {
Type origType = Type.cast(bar, List<int>);
} I'm afraid the feature could rather confusing and not very useful. |
@tatumizer wrote:
Right, the return type of void main() {
var t = Type.reify(String, Deserializable<String>);
if (t != null) {
// All succeeding.
assert(t == String);
assert(t is Type);
assert(t is Deserializable); // Which means `Deserializable<dynamic>`.
assert(t is Deserializable<String>);
}
}
Here's a version of the program that actually runs. It emulates the behavior of I had to fix a couple of bugs, but the overall structure works just fine: First, promotion of Here is the result: abstract class CallWithTypeParameters {
int get numberOfTypeParameters;
R callWithTypeParameter<R>(int index, R Function<X>() callback);
}
class RtoForList<E> implements Type, CallWithTypeParameters {
int get numberOfTypeParameters => 1;
R callWithTypeParameter<R>(int index, R Function<X>() callback) {
if (index != 1) throw ArgumentError("Expected index 1");
return callback<E>();
}
String toString() => 'RTO for List<$E>';
}
class NotList {}
Request? Type_reify<Reify, Request>() {
if (Request != CallWithTypeParameters) {
throw ArgumentError("Unsupported type");
}
return switch (Reify) {
const (List<List<List<NotList>>>) => RtoForList<List<List<NotList>>>(),
const (List<List<NotList>>) => RtoForList<List<NotList>>(),
const (List<NotList>) => RtoForList<NotList>(),
_ => null,
}
as Request?;
}
void f(Type rto) {
Object localRto = rto; // Enable promotion.
print(localRto);
if (localRto is! CallWithTypeParameters) {
return;
}
if (localRto.numberOfTypeParameters != 1) {
throw ArgumentError("Expected 1 type parameter");
}
var nextRto =
localRto.callWithTypeParameter(
1,
<X>() => Type_reify<X, CallWithTypeParameters>(),
)
as Type?;
if (nextRto == null) {
// No support for `CallWithTypeParameters`, just get the plain RTO.
nextRto = localRto.callWithTypeParameter(1, <X>() => X)!;
}
f(nextRto);
}
void main() {
f(RtoForList<List<List<NotList>>>());
} The crucial part is, as usual, the creation of a reified type object based on a given type (in this case: a type argument which is passed to The emulation in
Run it and see! The first invocation passes an RTO for If the result is null then the type argument doesn't support I'm sure this could be written more elegantly by working on a type parameter rather than on an RTO, but even with the RTO and with some type arguments that don't support
The reification (including
Then please use it to re-do this example. I find it highly unlikely that this would be possible. Having an RTO for |
Thanks for the example. I'm running it, inserting prints here and there, and have already come up with a couple of theories about its behavior. One of the conjectures is that the compiler is trying to solve the system of inequalities over X, R, and other letters. This process is ongoing, and the results will be reported at the end of the experiment. (BTW, how was I supposed to know all that? Could you provide a link to something not too academic?) Anyway, we have a parallel game on another board, let's switch to that for now. void f(Type rto) {
print(rto);
if (Type(rto).isSubtypeOf(List)) {
f(Type(rto).typeArguments[0]);
}
} I don't understand your criticism of the mechanism for being "weaker" than ex. open. You remember, I floated the idea of planting an ex. open into dart, but you were not excited about it, referring to some (unspecified) difficulties of implementation. Now, sure, we have to substitute a (possibly) weaker version to address that concern. BTW, how much weaker it is - not clear, it might be the same for all practical purposes. If the problem is "Having an RTO for List doesn't allow you to create a List" then it's solvable trivially: (Even without "instantiate", the generator can generate a map "type -> new instance", for all collection types encountered in the program, which is enough to implement generic deserialization). Footnotes
|
(Note that I haven't used those letters as anything other than the names of completely normal type parameters. No special compiler algorithms needed, but, of course, the standard ones like type inference can be applied according to the normal rules. But we can return to that later. ;-)
I think the missing part is that I'm proposing a mechanism which has one core behavior that we can't recreate in current Dart: The ability to create an instance of a compiler-generated (but completely normal, no-magic-needed) class That's it. The rest is normal Dart semantics. However, we need some syntax to specify how the compiler can generate that class. The clauses Next, we need some syntax to specify that a given type variable has a value that satisfies It is not a design which is known from other languages, but it does have conceptual connections to the notion of metaclasses (mostly known from dynamic languages like Smalltalk or CLOS), so I can't point to any papers where this idea has been further unfolded. Instead, the design and explanation work is what I've been doing here. ;-)
Consider an arbitrary method: class List<E> {
Iterable<T> map<T>(T toElement(E e)) {...}
...
} If RTOs are just as useful as types then we can go ahead and change this method such that it receives an RTO rather than a type argument: class List {
final Type e; // RTO of former `E`.
// Suitable constructors where `e` is initialized.
Iterable<Object?> map(Type t /*RTO of former T*/, Object? toElement(Object? e)) {...}
...
} We can't use
I've argued in favor of that idea for many years. However, it is such a massive effort that it isn't going to happen unless it get's promoted heavily (in particular: it's explicitly, consistently, eagerly, massively requested by the community). Even with massive support it's going to be a big effort because it introduces a new kind of types. (For example, it didn't take long for that kind of typing to be eliminated from the 'patterns' feature, because it was beyond reach with the given schedule. And patterns wasn't a small thing.) 'More capable type objects' is a tiny, lightweight thing in comparison. It doesn't give us exactly the same, but it does allow us to get the actual behavior associated with an existential open, and then we will have to insert a couple of type casts here and there because it isn't a full 'existential open' feature. I think that's a useful trade-off.
It looks like an invocation of a constructor of Anyway, we now have an instance of type So we can now call How would you type check this kind of object construction statically? If all these methods are members of the interface of To me it looks like it's getting less and less straightforward the more I try to understand what it would do.
As I mentioned, that won't suffice if you write a generic solution where the same code can handle more than one type. |
I've already figured that out. When
No, it won't conflict with anything, due to special syntax
But your method
You are biased! :-) I understand your point though: you are trying to advance the theory of generics, and I find the intention highly laudable. Maybe you can play on the second board more positively? Explore other things you can do with |
Right, the
It's a parameter (more specifically: a type parameter), and it is provided at each call site. It is quite uncommon to say that it is 'existential', and it is certainly not more existential than a value parameter: If you consider the function literal
Was it ever defined? It looks like a constructor invocation. Does it denote an object? Which one? What is the static type of that object? (Presumably, the members we can invoke will be the ones that belong to that static type.)
This sounds like Extension method invocation is statically resolved, which means that it is inherently unable to perform any kind of dispatch at run time. In other words, we can only use this kind of semantics in a situation where we could also invoke the given method statically (so if This contradicts the most fundamental purpose of the proposal in this issue: It's all about calling static members and/or constructors without knowing the exact type of the class that declares them. In other words, there's no way it could be relevant to talk about statically resolved invocations here. Could there be some other way to understand what you said such that the invocation is dispatched (that is, late-bound)? If it is dispatched (using anything like the normal OO method invocation semantics) then there must be an underlying object (and it isn't the RTO denoted by You can do it if
It is true that it may be able to throw in the case where multiple extensions can deliver an RTO of the desired type (we could also just return null), but there's a huge difference between the type safety of an operation which is invoked on a receiver of type We can also choose to restrict the expressive power of
I'm trying! However, doing type things with RTOs is just such a huge impediment that we don't have to impose on ourselves, and I honestly can't see how it could work. ;-) |
Consider extension E on String {
get length=>100;
}
main() {
print(E('abc').length); // prints 100
} This is the syntax of explicitly calling the method from the extension. (Please try it out). One may ask: what is the type (static, runtime, whatever) of the expression Now let's assume dart introduces a built-in extension named extension Types on Type {
Object? instantiate()=>... something
} This resolves the issue of what static type is and what it is not. var t=List<int>;
Object? list=Types(t).instantiate(); Sure, its static type of is class A {
List<List<int>> list;
static A fromJson(StringBuffer s) {
// generated code!
var a=A();
a.list = deserialize(List<List<int>>, s) as List<List<int>>;
// "deserialize" uses Types(t).instantiate recursively under the hoold
}
} Does it make sense? A separate question: assert(String == Rto<String>);
assert(Rto<String> is Type); I haven't thought it though. (Need a better name for Rto anyway) |
It is true that We could indeed have an extension (let's use the name extension TypeExt on Type {
Object? instantiate() => /*magic*/;
} Let's say that the "magic" code allows us to invoke a constructor whose name is the name of the class and whose parameter list allows for invocations with no arguments. However, honestly, this is a very rudimentary feature. First, as you mention, every object created in this way has static type Next, it only works with classes that have such a constructor (with that kind of name, and with a parameter list that allows us to pass no arguments). Consider the proposal in this issue: abstract class NoArgInstantiable<Self> {
Self call();
}
abstract class A {
int get aThing => 0;
}
class B1 implements A static implements NoArgInstantiable<B1> {
B1([int weCanAcceptOptionalArguments = 1]) {...}
int get b1Thing => 1;
}
class B2 implements A static implements NoArgInstantiable<B2> {
B2({String alsoNamedOnes = "Hello!"}) {...}
int get b2Thing => 2;
}
X foo<X static extends NoArgInstantiable<X>>() => X();
void main() {
// All statically typed.
var b1 = foo<B1>();
var b2 = foo<B2>();
var i = b1.b1Thing + b2.b2Thing;
A a = someCondition ? b1 : b2;
i += a.aThing;
} This is not the feature, this is just one example showing what it can do. In particular, we have created a static interface that allows us to express the constraint that a class must have a constructor with the name of the class that accepts the empty argument list (it can have optional parameters, it just has to allow for the invocation passing no args). We could also specify that it must accept one positional argument of a specific type (that we can parameterize as a type parameter on the static interface, if need be), or any other shape of formal parameter list. That is, we can just write the code which is needed in order to support any signature whatsoever, and also to support a constructor whose name is There are no name clashes because all those signatures will be expressed in separate classes similar to As you can see, This illustrates that the typing of late-bound object creation is just as strict as other kinds of Dart code. Here is how I think it would be done using abstract class A {
int get aThing => 1;
}
class B1 implements A { // Can't declare that it supports `instantiate()`.
B1([int weCanAcceptOptionalArguments = 1]) {...}
}
class B2 implements A { // Can't declare that it supports `instantiate()`.
B2({String alsoNamedOnes = "Hello!"}) {...}
}
X foo<X>() => TypeExt(X).instantiate() as X;
void main() {
var b1 = foo<B1>(); // Type `Object?`.
var b2 = foo<B2>(); // Ditto.
var i = (b1 as B1).b1Thing + (b2 as B2).b2Thing;
A a = (someCondition ? b1 : b2) as A;
i += a.aThing;
} I can't see how we can avoid dropping the static typing on the floor basically at every step in the code (and then we have to restore the type using
This feature is not reflection, it's a much smaller, simpler, and safer thing. The only piece of magic I'm proposing is the ability to create an instance of Even the implicitly generated classes could be written by hand. If we do that, though, then we'd need some other way to tell the compiler that this is the class (
I agree very much on the idea that it should be possible to introduce a mechanism which is a tiny corner of a reflective system which has no run-time cost for code that doesn't use this feature. It's just that I also want expressive power, and I want static typing wherever possible. |
I need time to process that. Meanwhile, I'm thinking of one fascinating possibility :-) A theory question. I don't understand how compiler handles generics in dart. In the languages where primitive types are not Objects (e.g. zig, julia, C++ etc) each invocation of a generic function may behind the scenes create a different version of the function, or struct, or something (it can be memoized to avoid repetition if called with the same type parameter from several places). What happens in dart - I have no idea (only conjectures). Could you point to some post where this is explained in non-academic language? ChatGpt was of no help: it characterized zig/C++ approach as "monomorphization", and noted that the languages with reified types (like dart) don't need that, but it doesn't tell me anything about how it actually works. What happens with all these Footnotes
|
Let's recall our initial exchange about the generator of deserializer for The argument is rather hazy, and I can't prove it as a theorem or something, but my conjecture is that while attempting to make these turtles (all the way down) statically safe, we would have to implement something equivalent to monomorpization - that is, generating a number of instances of deserializeList, each with specific parameter, which, in turn, would be equivalent (in terms of the amount of code, and also functionally) to the original setup where I generated these turtles by a generator. I admit that I don't know exactly what I'm talking about, but hope that it will somehow make more sense to you than it does to me :-) |
That's true! This is exactly the loophole that I'm using with One consequence of this is that it is not possible to obtain a function object that behaves in the same way as
That would be possible, surely, but the fact that the late-bound instance creation expression has such a general type is the main reason why I strongly prefer a mechanism that allows us to get a much more specific type. In the example that I showed here, It is also known statically that
No, there's no point in using late-bound invocations of static members or constructors if you know the exact type, you'd just do The only situation where this mechanism is useful at all is the case where the type isn't known at compile time: It is a type variable, or a type that contains type variables, and you want to call a constructor of that type (or a static member of the underlying class/enum/mixin). About generics in Dart: They are reified, which means that there is information which is encoded in the run-time state of the program that represents the value of each actual type argument (of each instance of a generic class, of each invocation of a generic function, etc). This is just like value parameters and instance variables: If you investigate the run-time representation of a In that sense, formal type parameters and actual type arguments are exactly the same kind of pair as formal (value) parameters and actual (value) arguments, except that (1) value arguments are objects and type arguments are types (which are not objects and cannot be the value of an expression in Dart), and (2) value arguments can only be omitted in the case where they are declared to be optional, but type arguments can generally be omitted, and they will then be provided by type inference (OK, the tools can say "I couldn't find a suitable type argument", but the basic approach is that type arguments can always be omitted, and then we get a compile-time error in those unusual situations where it isn't possible to compute them). This explains why A language that uses monomorphization will include constraints to ensure that it can be decided at compile-time exactly which actual type arguments are used at every location that applies a generic entity to a list of actual type arguments. If you don't have that kind of restriction on the expressive power of the type system then you can't monomorphize the program. However, when it is guaranteed that programs can be monomorphized, it's possible to eliminate all type arguments in a program by "macro expanding" every usage of a generic declaration. This basically means that we're inlining the actual type arguments into each instantiation of a generic class (for example, we'd create one copy of
That's not true! class A<X> {
A();
A<A<X>> get next => A<A<X>>();
}
main() {
var i = 20;
A a = A<Null>();
while (--i > 0) a = a.next;
print(a.runtimeType);
} This shows that there's no finite limit on the number of different types we can have at run-time. This also implies that it is undecidable in general to compute the value of a Dart term that denotes a type at compile-time. |
I understand:
But I think there's an option that could satisfy the most sophisticated taste: pattern matching based on (quasi-)static types. f<X>() {
print(X);
if (X typecase List<final Y>) f<Y>();
} Important: BTW, it doesn't make If this works, then consider something like X build<X, Y>(Y y) {
if (X typecase List<final Z>) {
return <X>[build<Z, Y>(y)];
} else if (X typecase Set<final Z>) {
return <X>{build<Z, Y>(y)};
} else if (Y typecase X) {
return y as X;
}
throw ArgumentError("Inconsistent call `build<$X, $Y>(_)");
} Here's a nicer form based on "switch without a scrutinee" X build<X, Y>(Y y) {
return switch {
X typecase List<final Z> => <X>[build<Z, Y>(y)].
X typecase Set<final Z> => <X>{build<Z, Y>(y)},
Y typecase X => y as X,
_ => throw ArgumentError(...),
}
} Generalizing based on static interfaces: X build<X, Y>(Y y) {
return switch {
X typecase CollectionKind<final Z>) => X<Z>.empty()..add(build<Z, Y>(y)),
Y typecase X => y as X,
_ => throw ArgumentError(...),
}
} where (improvising...) abstract class CollectionKind<C extends Collection<C>> {
Collection<C> empty();
}
abstract class Collection<E> {
void add(E elem);
} (We assume The problem is that there's nothing in the predicate I had a lively conversation with chatGPT about the issue. All variants it suggested (copied from other languages) are pure abstract nonsense. The approach based on static interfaces is much cleaner and more intuitive, but we have to find the best formalization. |
But the operands in your example are not constant type expressions: I can't see how this could be anything less than an existential open operation, and that's a significantly bigger thing than the proposal I've made here. It is also a quite different thing, because an existential open operation won't help us call static members and constructors. I do agree, though, that an existential open operation (using any syntax we might want for that) can be used to implement the The point I wanted to make when I mentioned the build example was actually the opposite: "Look, we can do it with this simple mechanism, we don't need a big and expensive thing like existential open to do it!" 😀 |
They are not constant type expressions, but they are not runtime types either. I tried to find the formal name for this kind of thing in the spec, but it's not there. The reason why this won't work with runtime types is that the true runtime types can have internal names like (making it up) If the compiler doesn't implement this kind of matcher, I can't easily see how you can implement a generic deserializer for lists that can handle
I don't understand that. I gave an example of Footnotes
|
They are not obtained as the run-time type of an object, but they are definitely "run-time types" in the sense that (1) we can't compute such types at compile time, that's in general an undecidable question, and (2) they have a run-time representation, which means that we can actually check what they are when a program execution reaches that part of the code.
How is your string based existential open going to handle the situation where more than one class in your program has the name How will it handle the situation where it encounters an How would it handle the bounds of the type variable? For example, with And so on. It's not reasonable to claim that you can easily do these things based on
There are several versions of The first two versions do not use metaobjects, but they aren't invoking any static members or constructors using late binding, so they cannot possibly be relevant when the claim is "the |
Of course you need this database! Is it a big deal? The late Mirrors used to have this information available. It's not a huge database. On the issue of two C from 2 different libraries - dart knows how to distinguish them today1, otherwise nothing would ever work! The problems you mentioned are solvable. I even think they are easily solvable, but I don't want to make provocative statements. :-)
Initially, I misunderstood the role of metaobjects. But my views evolved based on new information received during the discussion (which is the whole point of having any debate, to begin with). You need mataobjects, but they can be created behind the scenes without exposing anything like I have a clear mental picture of both the problem and the solution by now (maybe I'm missing some minor nuances, but that's OK) - so I consider the problem solved. Whether this solution will, or will not, be implemented, is orthogonal to the problem itself. If not, the problem can be reclassified as "recreational puzzle", which is fine with me. :-) Footnotes
|
I want to address this separately b/c the issue is fascinating in its own right
I'd call these types quasi-static. They can be computed in compile time (with some restrictions). The compiler can see the invocation of Some languages take variant 1 - and that's it. There's a hard limit on the complexity of type expressions. This is a pragmatic solution 1 The major issue here is this: does dart want to handle the problem properly or not. The emphasis here is on "want". If the answer is "no" then a good argument can be made in favor of doing nothing at all (it's even less work after all! :)) Footnotes
|
This issue is a response to #356 and other issues requesting virtual static methods or the ability to create a new instance based on a type variable, and similar features.
Static substitutability is hard
The main difficulty with existing proposals in this area is that the set of static members and constructors declared by any given class/mixin/enum/extension type declaration has no interface and no subtype relationships:
As a fundamental OO fact,
B
is an enhanced version ofA
when it comes to instance members (even in this case where we don't enhance anything), but it is simply completely unrelated when it comes to constructors and static members.In particular, the relationship between the constructors
A();
andB();
is very different from an override relationship.A
has a constructor namedA.named
butB
doesn't have a constructor namedB.named
. The static memberB.foo
does not overrideA.foo
.B
does not inheritA.bar
. In general, none of the mechanisms and constraints that allow subtype substitutability when it comes to instance members are available when it comes to "class members" (that is, static members and constructors).Consequently, it would be a massively breaking change to introduce a rule that requires subtype substitutability with respect to class members (apart from the fact that we would need to come up with a precise definition of what that means). This means that it is a highly non-trivial effort to introduce the concept which has been known as a 'static interface' in #356.
This comment mentions the approach which has been taken in C#. This issue suggests going in a different direction that seems like a better match for Dart. The main difference is that the C# approach introduces an actual static interface (static members in an interface that must be implemented by the class that claims to be an implementation of that interface). The approach proposed here transforms the static members into instance members, which means that we immediately have the entire language and all the normal subsumption mechanisms, we don't have to build an entirely new machine for static members.
What's the benefit?
It has been proposed many times, going back to 2013 at least, that an instance of
Type
that reifies a classC
should be able to do a number of things thatC
can do. E.g., if we can doC()
in order to obtain a new instance of the classC
then we should also be able to doMyType()
to obtain such an instance when we havevar MyType = C;
. Similarly forT()
whenT
is a type variable whose value isC
.Another set of requests in this topic area is that static members should be virtual. This is trivially true with this proposal because we're using instance members of the reified
Type
objects to manage the access to the static members.There are several different use cases. A major one is serialization/deserialization where we may frequently need to create instances of a class which is not statically known, and we may wish to call a "virtual static method".
Proposal
We introduce a new kind of type declaration header clause,
static implements
, which is used to indicate that the given declaration must satisfy some subtype-like constraints on the set of static members and constructors.The operand(s) of this clause are regular class/mixin/mixin-class declarations, and the subtype constraints are based on the instance members of these operands. In other words, they are supertypes (of "something"!) in a completely standard way (and the novelty arises because of that "something").
The core idea is that this "something" is a computed set of instance members, amounting to a correct override of each of the instance members of the combined interface of the
static implements
types.These declarations have no compile-time errors. The static analysis notes the
static implements
clause, computes the corresponding meta-member for each static member and for each constructor, and checks that the resulting set of meta-members amount to a correct and complete set of instance members for a class that implementsA<B>
. Here is the set of meta-members (note that they are implicitly created by the tools, not written by a person):The constructor named
B
becomes an instance method namedcall
that takes the same arguments and returns aB
. Similarly, the constructor namedB.named
becomes an instance method namednamed
. Static members become instance members, with the same name and the same signature.The point is that we can now change the result of type
Type
which is returned by evaluatingB
such that it includes this mixin.This implies that for each constructor and static member of
B
, we can call a corresponding instance member of itsType
:This shows that the given
Type
object has the required instance members, and we can use them to get the same effect as that of calling constructors and static members ofB
.We used the type
dynamic
above because those methods are not members of the interface ofType
. However, we could change the typing of type literal expressions such that are not justType
. They could beType & M
in every situation where it is known that the reified type has a given mixinM
. We would then be able to use the following typed approach:Next, we could treat members invoked on type variables specially, such that
T.baz()
means(T).baz()
. This turnsT
into an instance ofType
, which means that we have access to all the meta members of the type. This is a plausible treatment because type variables don't have static members (not even if and when we get static extensions), soT.baz()
is definitely an error today.We would need to consider exactly how to characterize a type variable as having a reified representation that has a certain interface. Let us use the following, based on the syntax of regular type parameter bounds:
Even if it turns out to be hard to handle type variables so smoothly, we could of course test it at run time:
Customized behavior
The behavior of the reified type objects can be customized, that is, they can do other things than just forwarding a call to a static member or a constructor.
One possible approach could be to have
static extends C with M1 .. Mk
in addition tostatic implements T1 .. Tn
on type introducing declarations (like classes and mixins), and then generate the code for the reified type object such that it becomes a subclass thatextends C with M1 .. Mk
and alsoimplements T1 .. Tn
. However, we could also apply those mixins outside thestatic extends
clause, so we only consider a simpler mechanism:A type introducing declaration can include a
static extends C
clause. This implies that the reified type object will be generated such that the givenC
is the superclass. Compile-time errors occur according to this treatment. E.g., ifC
is a sealed class from a different library thenstatic extends C
is an error, based on the fact that it would give rise to a subclass relation to that class, which is an error.This mechanism allows the reified type object to have arbitrary behaviors which can be written as code in the class which is being used as the
static extends
operand.Use case: An existential open mechanism
One particular kind of feature which could be very useful is a flexible mechanism that delivers the behaviors otherwise obtained by an existential open mechanism. In other words, a mechanism that allows the actual type arguments of a given class to be accessed as types.
This would not involve changes to the type system (so it's a much smaller feature than a real existential open would be). It is less strictly checked at compile time, but it will do the job—and it will presumably be used sparingly, in just that crucial bit of code that allows a given API to be more convenient to use, and the API itself would be statically typed just like any other part of the system.
For example, the reified type object could implement this interface:
A class
C
with a single type parameterX
could usestatic extends _CallWithOneTypeParameter<X>
, which would make it implementCallWithTypeParameters
:For example, assume that the standard collection classes will use this (note that this is a non-breaking change):
The 'existential open' feature is so general that it would make sense to expect system provided classes to support it. It also makes sense for this feature to be associated with a very general interface like
CallWithTypeParameters
, such that all the locations in code where this kind of feature is needed can rely on a well-known and widely available interface.If we have this feature then we can deconstruct a type of the form
List<T>
,Set<T>
, ..., and use the actual type argument:Obviously, this involves some delicate low-level coding, and it needs to be done carefully. However, the resulting API may be considerably more convenient than the alternatives.
In particular, an API could use regular objects that "represent" the composite types and their type arguments. Those type representations would then be handled by an interpreter inside
build
. For example, with this kind of approach it is probably not possible to obtain a helpful return type, and it is certainly not possible to use type inference to obtain an object that "represents" a type likeSet<List<int>>
.Running code, emulating the example above.
Type parameter management
With this mechanism, a number of classes will be implicitly induced (that is, the compiler will generate them), and they will be used to create the reified type object which is obtained by evaluating the corresponding type as an expression.
The generated class will always have exactly the same type parameter declarations as the target class: If class
C
has 3 type parameters with specific bounds then the generated class will have the same type parameter declarations with the same bounds. This implies thatT
instatic implements T
orstatic extends T
is well defined.More capable type objects as an extension
We may well wish to equip an existing type that we aren't able to edit (say,
String
) with reified type object support for a particular class.We can do this by adding a "magic" static member on the class
Type
as follows:This is, at first, just a less convenient way to reify a type (passed as the type argument
TypeToReify
) into a reified type object. With an actual type argument ofType
(or any supertype thereof), the returned object is simply going to be the reified type object that you would also get by simply evaluating the givenTypeToReify
as an expression.However, if
DesiredType
is not a supertype ofType
then the reified type object may or may not satisfy the type constraint (that is, it may or may not be an instance of a subtype ofDesiredType
). If it is not an instance of the specifiedDesiredType
thenreify
will attempt to use an extension of the reified type ofTypeToReify
.Such extensions can be declared in an
extension
declaration. For example:At the location where
Type.reify<List<int>, CallWithTypeParameters>()
is invoked, we gather all extensions in scope (declared in the same library, or imported from some other library), whose on-type can be instantiated to be the givenTypeToReify
. In an example where the givenTypeToReify
isList<int>
, we're matching it withList<X>
.For this matching process, the first step is to search the superinterface graph of the
TypeToReify
to find the class which is the on-type of the extension. In the example there is no need to go to a superinterface, theTypeToReify
and the on-type of the extension are already both of the formList<_>
.Next, the value of the actual type arguments in the chosen superinterface of the
TypeToReify
is bound to the corresponding type parameter of the extension. WithList<int>
matched toList<X>
,X
is bound toint
.Next, it is checked whether there is a
static implements T
orstatic extends T
clause in the extension such that the result of substituting the actual type arguments for the type parameters inT
is a subtype ofDesiredType
.In the example where
TypeToReify
isList<int>
andDesiredType
isCallWithTypeParameters
, we find the substitutedstatic implements
type to be_CallWithOneTypeParameter<int>
, which is indeed a subtype ofCallWithTypeParameters
.If more than one extension provides a candidate for the result, a compile-time error occurs.
Otherwise, the given reified type object is returned. In the example, this will be an instance of the implicitly generated class for this
static implements
clause:The result is that we can write
static implements
andstatic extends
clauses in extensions, and as long as we're using the long-form reificationType.reify<MyTypeVariable, CallWithTypeParameters>()
, we can obtain an "alternative reified object" which is specifically tailored to handle the task thatCallWithTypeParameters
was designed for.If we're asking for some other type then we might get it from the reified type object of the class itself, or perhaps from some other extension.
Finally, if the reified type object of the class/mixin/etc. itself doesn't have the
DesiredType
, and no extensions will provide one, thenType.reify
returns null.It is going to be slightly less convenient to use
Type.reify
than it is to simply evaluate the type literal as an expression, but the added expressive power will probably imply thatType.reify
will be used for all the more complex cases.Revisions
on Type
.The text was updated successfully, but these errors were encountered: