-
Notifications
You must be signed in to change notification settings - Fork 207
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
Give Type
a type argument such that T is Type<T>
#2090
Comments
We have considered this before. We decided against making Currently a For static extension methods, let's do it properly instead! #723. (No, that won't work for For checking the type relations of type variables, do that directly. #459 I want people to stop trying to use |
+1 for special-casing |
@lrhn, we did indeed discuss introducing an existential-open construct several times, e.g., based on type patterns (#170): void foo(List xs, Object? o) {
xs as List<var X>; // Guaranteed to not throw; will bind `X` to the actual type argument.
// `xs` is now promoted to `List<X>`.
if (o is X) xs.add(o); // No dynamic type check needed.
} That mechanism will allow programs to access the value of type arguments of existing objects at their run-time type or any superinterface. As soon as a given library has imported The ability to extract the actual value of all type arguments of all classes in scope would presumably make it very difficult to erase type parameters, so that's definitely a cost which is associated with existential-open constructs of any kind. However, changing the built-in Note also that a With that in mind, I believe that it is simply irrelevant to think about the properties of existential-open mechanisms in order to say anything about the implications of adding a type argument to
Perhaps you could provide an example showing how the addition of a type parameter to
I'm not so happy about the syntax |
Type
gets a type argument such that T is Type<T>
Type
a type argument such that T is Type<T>
If the code contains If we can open a generic type, like If we make Without an "open type" or binding run-time type variable pattern match, it's probably not a problem. That's why making Now, if we remove |
Indeed, and that is also necessary in order to compile code where a
That is again true if the code creates a list (or any other instance of a generic class) where that 'any' type is used as a type argument. Again there's nothing special about
.. then we can go further: If a class However, we don't even do that! For instance, developers could
And how would you create an instance whose run-time type is However, the access to an instance of So the existential open does make a difference, but that difference exists with or without We could of course remove It's a little bit like mutable state: Pure functional languages exist (so-so), but even Haskell uses monads to provide access to effectful computations (and it has unsafe stuff as well). Even if you can write the software you need without a certain (ugly?) language mechanism, it is not necessarily the best trade-off for all developers at all times. And I happen to think that a very low level of support for reified types is likely to be justified; and this proposal is intended to make that feature a bit more statically analyzable.
The point is that |
By having an instance of If I have a If If we can then also do Now, arguably, if we allow |
I don't think it's possible to maintain correct OO semantics unless each object has a representation that offers access to a representation of the type of the object. (C++ does it with structs, but they are by design just storage areas with associated non-virtual functions, and I don't think a compiler is going to be able to turn many Dart classes into structs in that sense). So if we have an object whose run-time type is
What I'm saying is that you can't have a specific type But nobody said that there is an instance of But it doesn't actually matter, because there is a representation of every actual type argument of instances in the heap, so there would not be anything new in being able to obtain yet another copy of such a representation.
An instance of I do not believe that any particular optimizations will be enabled just because it is known that
Most type checks are of the form Checking for a type variable requires more elaborate behavior, and that is, as far as I know, handled by a routine in the runtime of the VM. I don't know if anything can be optimized if we can guarantee that the set of classes where a subtype test can occur is smaller than the set of classes in the program. It would be more interesting if type parameters could be erased, but, as mentioned, I do not believe we do that, and I suspect that modern Dart would not allow for doing that to any significant extent anyway. |
@mkustermann, @mraleph, we're going completely into the woods here! Perhaps you could comment? ;-) Does it sound like a serious performance hit if we were to support the change of |
The "representation of a type" is not a single monolithic thing. Parts of it can be individually tree-shaken, like specific methods that are not used in the program, or even the ability to do It's AoT compiled code that is affected, not the standalone VM, so there is no runtime-call to do the checking. |
I think dart2js (/cc @rakudrama) is always more impacted by changes like these, because their type representation is modularised enough for them to treeshake things at finer granularity. VM does not tree-shake things at this level - generic runtime support to construct arbitrary types and type check against them is always included. If So the current state of VM tree-shaking does not really care about this change unless I miss something subtle.
|
hi, is there any progress on this issue? |
No decision yet, but the issue serves to make the proposal concrete and keeping it in sight. @rakudrama, the proposal here is to make |
It is unclear whether the proposal will create types that are currently are equal but not equal with the type parameter
dart2js has an optimization that elides type variables from classes and methods when the type variable is used only to inform Creating constant |
Thanks for the input, @rakudrama!
If we have two reified types In particular, the run-time types of
First,
This is very interesting! I would assume that any reification of a type variable (e.g., However, it seems reasonable to say that
This is great to know, thanks! This is the first real cost I've seen in relation to |
If this is a problem, there is always the option to only special-case |
The problem here is that there are now two dart2js canonicalizes all along the chain type expression ⟶ If you redefine We could maintain primitive equality if |
I think that should be covered by the following:
So the case expressions and the keys of An identity check could fail in the case where we try to use a non-constant reified type to select the case or lookup the map value, but that's true today as well: var map = Map<Type, int>.identity()..[List<int>] = 1;
Type listOf<X>() => List<X>;
void main() {
print(map[List<int>]); // '1'.
print(map[listOf<int>()]); // 'null' on the vm.
}
This is already true in the vm, so the converse is not a property that Dart developers can rely on in general. But I understand that dart2js canonicalizes some reified types even in the case where they are constructed at run time (like However, the following seems to allow for a solution anyway:
That would certainly have the desired properties, it would still true that |
The "hack" that we use in |
Let me repeat an obvious question here: @lrhn wrote:
Perhaps you could provide an example showing how the addition of a type parameter to The only cost that actually came up during the discussions in this issue is the following:
It would be interesting to know whether this is typically 500 nanoseconds going to 1 microsecond, or something much bigger. |
In the current language, having Even without that, there is might still be a need to retain extra type information, if every type in the program an occur as a type parameter to a generic class. There is no limit to the type arguments that can occur in the value of |
With this proposal, that is still true. You need to assume another feature in order to go beyond that:
Oh, I'd like that, too! ;-) But assume that you have an instance If you obtained If you obtained Here's a possible counterargument: "OK, but the body of the type pattern is new code, and nobody did the exact same things at L that they are now doing in the body of the type pattern". We need to consider programs doing the same work (it's not hard to prove that if you edit your program to do new things, it may run more slowly). My point is that you could already do the same things if you create, say, a function literal in the same scope as L, and pass the function object along with So there's nothing new that you can do, and hence the program without In other words, there is no new cost for a program that does the same thing (perhaps more conveniently) if we add support for The only new cost I can see is still that creation of reified types during constant evaluation needs to create instances of |
Assume that you can tree-shake the implementation of checking whether a type variable matches a particular type. Say the expression As things currently are, a type cannot be the value of If we can write: R callWithTypeOf<R>(Object? o, R Function<T>(T value) f) =>
switch (o.runtimeType) {
case Type<var T> _ => return f<T>(o as T); // Exhaustive.
}
R callWithType<R>(Type type, R Function<T>() f) =>
switch (o.runtimeType) {
case Type<var T> _ => return f<T>(); // Exhaustive.
} then that closure property goes out the windows. Any type which has an instance, or which occurs as a And then we can't tree-shake as much of the information needed to do It's not about tree-shaking entire classes. It's about tree-shaking some metadata for the class when that meta-data is not used by the program. Dart2js does this today, and would be affected on the compiled size of programs if we make that tree-shaking less efficient. |
This is not true. For example: void f<X>(int count) {
print(X);
if (count > 0) f<List<X>>(count - 1);
}
void main() {
f<int>(5);
print('... and so on.');
} It is true that every possible type argument in a program is of the form |
It's true that the concrete types are not limited, but this is about metadata for classes, not types. For the practical purpose here, we might be able to treat a type argument of The metadata needed to use that type is only the metadata of |
I guess the real topic here (in the crusade against If a given One question that arises immediately is whether this program should run without dynamic errors?: void main() {
A<int> a = A<String>() as dynamic;
} We cannot expect to detect statically that the value of the initializing expression has type Is that OK, based on the reasoning that there is no soundness issue, because a If this is not OK, then I can't really see how a program that contains Conversely, if the program that doesn't use an existential open also doesn't contain any type annotation or type test with a type of the form And if we're not looking at the ability/lack-of-ability to eliminate some type arguments at run time, what are we talking about? |
No. There is no (big) problem with The type arguments applied to We are talking about retaining some information (maybe metadata, possibly stub code) which is only used in particular code patterns. |
I was trying to find a very concrete situation where there is a cost (instances of some classes need respectively do not need a run-time representation of their actual type arguments). I couldn't see that there will be an added cost for a program using existential open, compared with a program that does something which in some way could be considered to be the same thing. Could you make that metadata more concrete? It is not obvious to me that it maps to anything that actually exists at run time. Another thing is that I find it hard to believe that the demands on the representation of a type |
I have a hard time pointing to a specific such optimization, since I only know about them secondhand. And it was years ago that I heard about it (but I assume we won't remove a functioning optimization, especially on the web). @rakudrama Is it still true that Dart2js avoids reifying parts of the runtime support for type checks involving type parameters, for types which cannot be type arguments? (Is it possibly true for other AoT compilers?) |
@rakudrama, it would be great if you could comment on one more thing, in addition to the question from @lrhn: Consider a class (generic or non-generic) If situation (1) exists, would the existence of situation (2) create a need to retain any additional information at run time? Is the information required in situation (2) a subset of the information for situation (1), or a superset, or just a different set? I'd certainly expect situation (1) to require that we retain information about the implementation of some methods that an instance of |
For those who need a solution that works now, I've just uploaded a package that provides some of the features a However the package is limited and doesn't really solve the issue, just abuses type generics to do type checking in the same way as |
That's cool, @abitofevrything! Of course, here is one difference between the situation where the reified type for So let's say that we've handled that issue for all types where we need this feature. The library example.dart does this by having a class Value<X> {
final XType = RuntimeType<X>();
... // Other members (including constructors) same as without RuntimeType.
} Let's just assume that However, there's one place where class List<E> {
void add(E e) { print('Added $e!'); }
Y callWithTypeArgument<Y>(Y Function<X>(List<E>) callback) =>
callback<E>(this);
}
void main() {
List<num> xs = List<int>();
xs.callWithTypeArgument<void>(<E>(this_) {
final newElement = 1;
if (newElement is E) {
this_.add(newElement); // Guaranteed to succeed.
}
});
} The package 'runtime_type' does define I actually think that |
Given the following code: T castNum<T extends num>(num number) => number as T;
final cast = castNum(1.0); // infers num
final castToInt = castNum<int>(1.0); // infers int
final castToDouble = castNum<double>(1.0); //infers double
final castToString = castNum<String>(1.0); // compile error Would this proposal allow something like this? T castNum<T extends num>(num number, Type<T> type) => number as T; // `as T` or `as type`?
final cast = castNum(1.0); // compile-time error, we could give `type` a default if we really want to
final castToInt = castNum(1.0, int); // infers int
final castToDouble = castNum(1.0, double); //infers double
final castToString = castNum(1.0, String); // compile-time error For this to work I guess this would need to be allowed: Type<num> numType = num;
Type<num> intType = int; // Ok, int extends num
Type<num> stringType = String // compile-time error |
@rubenferreira97 The final castToInt = castNum(1.0, int); // infers int would have no context type, a type parameter of You can test this today as: class Typ<T> {}
void main() {
T castNum<T extends num>(num number, Typ<T> type) {
print(T);
return number as T;
}
final castToInt = castNum(1.5, Typ<int>());
} which compiles and at runtime it prints |
@rubenferreira97 wrote:
Yes, the type argument of
The proposal in this issue does not involve any support for type tests or type casts on reified types (so we can't use |
I think this may be related, but I ran into a painful issue today and replicated a Dartpad to show the issue: In my case, I was expecting to add Types to a list and then check if a variable is of that type. It did not work as expected, but using runtimeType seems to. However, @eernstg and @lrhn pointed out that I may want to avoid runtime type. |
@bradcypert try to use |
Angular DI case: class ClassProvider<T, U extends T> extends Provider<T> {
ClassProvider(this.type, {required this.useClass});
final Type<T> type;
final Type<U> useClass;
}
const ClassProvider(Client, useClass: BrowserClient); Or if this would be possible: class ClassProvider<T> extends Provider<T> {
ClassProvider(this.type, {required this.useClass});
final Type<T> type;
final Type<U extends T> useClass;
} |
@bradcypert, considering https://dartpad.dev/?id=bb13e2f585a49ade106fe764e4089af8, it looks like you're maintaining a collection of The feature proposed in this issue won't change the treatment of class Typer<X> {
const Typer();
Type get asType => X;
bool isTypeOf(Object? o) => o is X;
bool isSubtypeOf<Y>() => <X>[] is List<Y>;
bool isSupertypeOf<Y>() => <Y>[] is List<X>;
R callWith<R>(R Function<Z>() callback) => callback<X>();
bool operator ==(Object other) {
if (other is! Typer) return false;
return other.asType == X;
}
bool operator <=(Typer other) => other.callWith(<Z>() => isSubtypeOf<Z>());
bool operator >=(Typer other) => other.callWith(<Z>() => isSupertypeOf<Z>());
bool operator <(Typer other) => other != this && this <= other;
bool operator >(Typer other) => other != this && this >= other;
}
class Monster {}
class Slime extends Monster {}
class Wolf extends Monster {}
class WolfImpl extends Wolf {}
final w = WolfImpl();
void main() {
const typers = [Typer<Slime>(), Typer<Wolf>(), Typer<WolfImpl>()];
print(typers.any((typer) => typer.isTypeOf(w))); // 'true';.
var xs = typers[1].callWith(<X>() => <X>[]);
print(xs.runtimeType); // 'List<Wolf>'.
print(typers[2] < typers[1]); // `true`, confirming `WolfImpl <: Wolf` and `WolfImpl != Wolf`.
} If you own all the relevant code then it should be rather easy to use |
@ykmnkmi, the second version using class ClassProvider<T, U extends T> extends Provider<T> {
ClassProvider(this.type, {required this.useClass});
final Type<T> type;
final Type<U> useClass;
} One thing to keep in mind here is that there is a variance issue: The type system will be perfectly happy with this: var provider = ClassProvider<Object?, Object?>(int, useClass: String); However, if we get support for use-site invariance then you could use the following: class ClassProvider<T, U extends T> extends Provider<T> {
ClassProvider(this.type, {required this.useClass});
final Type<exactly T> type;
final Type<U> useClass;
} This means that if you call the constructor with the first argument For now (that is, assuming that we have the feature proposed in this issue "now" ;-), you'd have to use a dynamic check to enforce the invariance: class ClassProvider<T, U extends T> extends Provider<T> {
ClassProvider(this.type, {required this.useClass}) : assert(type == T);
final Type<T> type;
final Type<U> useClass;
} |
Perhaps it's irrelevant, but is there any feature proposal for omitting the second type argument here? class ClassProvider<T, U extends T> extends Provider<T> {} |
Ah, but of course, I didn't think about that: class ClassProvider<T> extends Provider<T> {
ClassProvider(this.type, {required this.useClass});
final Type<exactly T> type;
final Type<T> useClass;
} So if you do not plan to write any code in By the way, one thing you could do would be this: class ClassProvider<T> extends Provider<T> {
ClassProvider(this.type, {required this.useClass}) : assert(type == T);
final Type<T> type;
final Type<T> useClass;
} This would (when assertions are enabled) cause the construction of a |
This issue is a proposal to change the built-in class
Type
such that it accepts a single type parameter with no bound, and to change instances ofType
that reify a Dart type, such that they satisfyt is Type<T>
whenevert
reifiesT
.Note that this feature has been mentioned in several different discussions, e.g., in connection with abstract static methods and virtual static methods, #356. This issue ensures that we have a specific location where the concept is explicitly proposed.
Motivation
The main motivation for this feature is that it allows for an improved static analysis of reified types.
In particular, we could express something which is similar to static extensions (if we allow
int.foo()
to call an extension method on a receiver of typeType<int>
, otherwise we'd need to use(int).foo()
):Next, it would be possible to determine statically that a type variable satisfies a bound which is stronger than the declared one. For example, we could allow the following:
Another example usage is dependency injection, that is, factories associated with class hierarchies:
Static Analysis
A type literal
t
which is statically known to be a reification of a typeT
is statically known to have typeType<T>
. Otherwise,Type<T>
is treated just like any other generic type.An expression of the form
X is Type<T>
whereX
is a type variable would promoteX
toX extends T
in the true continuation, in the case whereT
is a subtype of the bound ofX
.Dynamic Semantics
A reified type
t
that reifies the typeT
has a run time type such thatt is Type<S>
is true if and only ifT <: S
.The text was updated successfully, but these errors were encountered: