-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Type parameter is not preserved in type-erased context #54624
Comments
I tried abstract class Foo<inout T extends Object> {
// ^^^^^
List<T> read();
void write(List<T> value);
}
// ...
Foo fooByName(String name) {
switch (name) {
case 's': return StringFoo() as Foo;
// ^^^^^^
case 'i': return IntFoo() as Foo;
// ^^^^^^
default: throw 'bad name $name';
}
} |
This is working as expected. And I don't see a better way to avoid it than the inlining that you do. The static type of If However, if you create a new list, like The reason All of this is mostly unavodiable for those particular functions. I can't see a good way to avoid that without inlining, so that the call to You have an unsafely covariant class, an generic parameter type, and an actual value which is a proper subtype of the static type. That means you can never use the static type for anything that is retained at runtime, because then it might not match the actual runtime type. A static type system means that you can't have types that depends on runtime values. That's why |
@Mike278, it is indeed working as intended, but this doesn't mean that it must work like that. What you need is actually an existential open feature, that is, a construct that will allow you to "discover" and use the value of a type parameter from outside the location where that type parameter is in scope. We don't have that as a general language mechanism at this time, but for your own classes (or indeed for any class where you have the opportunity to edit the code) you can get the same effect as a special case by adding a suitable instance method to the class. Here is the example, adjusted to have and use such a method ( // `Foo` classes.
abstract class Foo<T extends Object> {
List<T> read();
void write(List<T> value);
R callWithT<R>(R callback<X extends Object>(Foo<X> self)) =>
callback<T>(this);
}
class StringFoo extends Foo<String> {
@override
List<String> read() => ['hello', 'world'];
@override
void write(List<String> values) {
print('writing strings $values');
}
}
class IntFoo extends Foo<int> {
@override
List<int> read() => [1, 2, 3];
@override
void write(List<int> values) {
print('writing ints $values');
}
}
// Functions operating on `Foo` classes.
void processFoo<T extends Object>(Foo<T> f) {
f.callWithT<void>(<X extends Object>(self) {
final data = self.read();
// do some processing...
self.write(data);
});
}
void processFoo2<T extends Object>(Foo<T> f) {
f.callWithT<void>(<X extends Object>(self) {
final data = self.read();
// do some processing...
self.write([
for (final (i, x) in data.indexed)
if (i % 2 == 0) x
]);
});
}
Foo<Object> fooByName(String name) {
switch (name) {
case 's':
return StringFoo();
case 'i':
return IntFoo();
default:
throw 'bad name $name';
}
}
void processFooByName(String name) {
final foo = fooByName(name);
processFoo(foo);
processFoo2(foo);
}
void main() {
processFoo(IntFoo());
processFoo2(IntFoo());
processFooByName('i');
processFooByName('s');
} When you get to this piece of code self.write([
for (final (i, x) in data.indexed)
if (i % 2 == 0) x
]); you have a suitable typing of In particular, the value of The crucial point is that we can now use In the body of the One consequence of this is that the list literal used in the invocation of You can see that it works by running the code. The main considerations are something like:
Of course, you can use a shortcut in cases where the code is already known to be type safe even without an invocation of final data = f.read();
// do some processing...
f.write(data); is safe even without the existential open (if |
I think we can close this issue: The discussion shows that the observed behavior is in line with the specification, it mentions that an existential open (emulation) can be used to eliminate the covariance related type errors, and no bugs are reported. PS: It should be noted that we could have used statically checked variance as well (that is, we could have changed |
Thank you @lrhn and @eernstg for your replies! I think I mostly understand, but I have 2 follow-up questions:
class Summary<T extends Object> {/*...*/}
abstract class Foo<T extends Object> {
List<T> read();
Summary<T> write(List<T> value);
}
Summary<T> processFoo<T extends Object>(Foo<T> f) {
final data = f.read();
// do some processing...
return f.write(data);
} With this additional consideration, I couldn't figure out how to spell Summary<T> processFoo<T extends Object>(Foo<T> f) {
return f.callWithT<Summary<T>>(<X extends Object>(f) {
final data = f.read();
// do some processing...
return f.write([for (final (i, x) in data.indexed) if (i % 2 == 0) x]); // `Summary<X>` isn't a `Summary<T>`
});
} I also tried implementing the "inline" approach in my real program and encountered a few more surprises. This triggers the error: Iterable<Summary<Object>> processFoosByName(List<String> names) {
return names.map(
(name) => switch (name) {
's' => processFoo(StringFoo()),
'i' => processFoo(IntFoo()),
_ => throw 'bad name $name',
},
);
} but calling |
Thanks!
That's because statically checked variance support (dart-lang/language#524) has not yet been fully implemented: The compile-time checks will take variance into account, but the backends have not yet implemented the support for invariance and contravariance, and this means that the cast You can emulate the full semantics of invariance, though. Here's a slightly shorter version using (partially implemented) statically checked variance: // Use '--enable-experiment=variance'
abstract class Foo<inout T extends Object> {
List<T> read() => [];
void write(List<T> value) {}
}
class IntFoo extends Foo<int> {}
class StringFoo extends Foo<String> {}
Foo fooByName(String name) {
switch (name) {
case 's': return StringFoo() as Foo; // Succeeds, incorrectly.
case 'i': return IntFoo() as Foo; // Succeeds, incorrectly.
default: throw 'bad name $name';
}
}
void main() {
fooByName('i');
fooByName('s');
} The same thing, emulated: typedef Inv<X> = X Function(X);
typedef Foo<T extends Object> = _Foo<T, Inv<T>>;
abstract class _Foo<T extends Object, Invariance extends Inv<T>> {
List<T> read() => [];
void write(List<T> value) {}
}
class IntFoo extends Foo<int> {}
class StringFoo extends Foo<String> {}
Foo fooByName(String name) {
switch (name) {
case 's': return StringFoo() as Foo; // Throws, as it should.
case 'i': return IntFoo() as Foo; // Throws, as it should.
default: throw 'bad name $name';
}
}
This is the point where we need to help the type system a little bit: It is actually true that That's not nice, but there is no way that I can think of which will establish as a statically known property that class Summary<T extends Object> {/*...*/}
abstract class Foo<T extends Object> {
List<T> read();
Summary<T> write(List<T> value);
R callWithT<R>(R callback<X extends Object>(Foo<X> self)) => callback(this);
}
/*
Summary<T> processFoo<T extends Object>(Foo<T> f) {
final data = f.read();
// do some processing...
return f.write(data);
}
*/
Summary<T> processFoo<T extends Object>(Foo<T> f) {
return f.callWithT<Summary<T>>(<X extends Object>(f) { // The type argument would be inferred, too.
final data = f.read();
// do some processing...
return f.write([
for (final (i, x) in data.indexed)
if (i % 2 == 0) x
]) as Summary<T>; // Using guaranteed property `X <: T`.
});
} We could of course also test this using The |
Ah gotcha, I didn't realize it shouldn't have worked! I actually first tried the emulated invariance approach, as I'd seen it suggested in some other similar issues, and I got the compile-time error on
Thanks, this helped it click for me. |
I have a class like this:
And some subclasses that specify a concrete type parameter:
And some top level functions that operate on
Foo<T>
s:This works well in places where
processFoo
is called with a concrete type:There are other places where I want to call
processFoo
with an instance ofFoo
whose concrete type is not known at compile time. For example:This also seems to work:
Months later I made what I thought was a minor refactor to
processFoo
processFoo(IntFoo());
still works fine, but nowprocessFooByName('i');
throws a runtime type error:type 'List<Object>' is not a subtype of type 'List<int>' of 'values'
.I understand that
fooByName
has no choice but to return aFoo<Object>
, and I understand that the runtime typecheck is required because Dart's class generics are unsafely covariant, but I can't seem to figure out how to implementprocessFooByName
so that it doesn't throw, other than by inliningfooByName
like this:Is there any other way to preserve
Foo
's type argument in a "type-erased" context?The text was updated successfully, but these errors were encountered: