-
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
generic functions throw TypeError once generic information is lost #56231
Comments
Summary: The code throws a |
note that reintroducing the generic type again will make it work: for (var element in genericClassList) {
Object? result;
if (element is GenericClass<String>) {
result = element.converter(element.exampleValue);
} else if (element is GenericClass<int>) {
result = element.converter(element.exampleValue);
}
assert(result == element.exampleValue);
} and completely removing type information will also make it work: for (var element in genericClassList) {
final result = (element as dynamic).converter(element.exampleValue);
assert(result == element.exampleValue);
} |
@ahmednfwela, I'm afraid you are using a dangerous combination of features. I created #59050 in order to request a diagnostic message when it occurs, such that developers will know that they shouldn't do this, or at least that they should be very careful when doing it. You can vote for that issue. ;-) The core point is that class GenericClass<T> {
final Object? Function(T converter) converter;
GenericClass(this.converter);
}
void main() {
GenericClass<num> g = GenericClass<int>((_) => 0);
g.converter; // Throws, you don't even have to call it.
} It throws because the static type of So the crucial point is that The reason why it helps to promote The reason why it helps to use The simplest fix is to avoid using "contravariant members" at all. That is, don't declare an instance variable (final or not, that doesn't matter) whose type is a function type where a type variable of the class occurs as a parameter type of the function type. There is another way to make the construct statically type safe, but it is somewhat involved: What you really need is to make the class invariant in that type argument. We may add statically checked variance to Dart (see dart-lang/language#524), in which case we can do this: // Needs `--enable-experiment=variance`
class GenericClass<inout T> {...}
... If you do that then you will get a compile-time error at the initialization of So this means that you'll get a heads-up when the problem is created, and you may be able to eliminate the problem at that point. |
@eernstg thanks for the quick reply and explanation, I didn't know such a bug existed in dart for so long can you also please explain why changing the structure of the class to use inheritance instead of passing variables makes it work ? abstract class GenericClass<T> {
const GenericClass({required this.exampleValue});
Object? converter(T value);
final T exampleValue;
}
class StringVariant extends GenericClass<String> {
const StringVariant({required super.exampleValue});
@override
Object? converter(String value) {
return value;
}
}
class IntVariant extends GenericClass<int> {
const IntVariant({required super.exampleValue});
@override
Object? converter(int value) {
return value;
}
}
void main() {
final genericClassList = <GenericClass>[
StringVariant(exampleValue: 'hello'),
IntVariant(exampleValue: 10),
];
for (var element in genericClassList) {
element.converter(element.exampleValue);
}
} |
another workaround I discovered by doing inheritance: abstract class GenericClass<T> {
Object? converter(T converter);
const GenericClass();
}
class GenericClassVariant<T> extends GenericClass<T> {
const GenericClassVariant(this.converterFunction);
final Object? Function(T value) converterFunction;
@override
Object? converter(T converter) {
return converterFunction(converter);
}
}
void main() {
GenericClass<num> g = GenericClassVariant<int>((_) => 0);
g.converter(5); // Works!
} |
Yes, when a type parameter occurs contravariantly it is in general much safer to use an instance method where some parameter has a type which is a type parameter of the class than it is to have an instance variable whose type is a function type with the same parameter type. This is because method tear-offs get their type adjusted when torn off, and also because you may not need to tear off the method in the first place. class A<X> {
void f(X x) {}
}
class B<X> {
void Function(X) f;
B(this.f);
}
void main() {
A<num> a = A<int>();
B<num> b = B<int>((_) {});
a.f(1); // OK, just calls `a.f` with arguments.
b.f(1); // Throws because we evaluate `b.f`, with a type check (and then we'd call it, except that we don't get that far).
} You can even tear off the instance method (as in We will need to check the type of the argument at some point, but with the instance method we can do it late, and with the instance variable we must do it already when someone gets hold of the function object. The approach that uses |
thanks for the explanation, this is the most generic workaround I have found for this: abstract class _FunctionWrapper<TOut, TIn> {
const _FunctionWrapper();
TOut call(TIn value);
}
class FunctionWrapper<TOut, TIn> extends _FunctionWrapper<TOut, TIn> {
final TOut Function(TIn value) _function;
const FunctionWrapper(this._function);
@override
TOut call(TIn value) {
return _function(value);
}
} I am not sure if extension types can be used here to reduce the memory overhead of creating an object per function, but if you know a way to make it possible that would be great Edit: found it: extension type const FunctionWrapper<TOut, TIn>(Function fn) {
TOut call(TIn value) {
return fn(value) as TOut;
}
} however this accepts any function of any type, which might not be preferrable. |
Here is a rather general approach that does two things: (1) It emulates invariance for Object? _string(String value) {
return value;
}
Object? _int(int value) {
return value;
}
// Add a superclass to abstract away the type argument.
abstract class GenericClassBase {
R callWithTypeArgument<R>(R Function<S>(GenericClass<S> self) callback);
}
// Emulate invariance for `GenericClass`.
typedef Inv<X> = X Function(X);
typedef GenericClass<T> = _GenericClass<T, Inv<T>>;
class _GenericClass<T, Invariance extends Inv<T>> implements GenericClassBase {
final Object? Function(T converter) converter;
final T exampleValue;
const _GenericClass({
required this.converter,
required this.exampleValue,
});
R callWithTypeArgument<R>(R Function<S>(GenericClass<S> self) callback) =>
callback<T>(this);
}
const stringVariant =
GenericClass<String>(converter: _string, exampleValue: "hello");
const intVariant = GenericClass<int>(converter: _int, exampleValue: 10);
void main() {
final genericClassList = <GenericClassBase>[
stringVariant,
intVariant,
];
for (var element in genericClassList) {
element.callWithTypeArgument(<T>(typedElement) {
final converter = typedElement.converter;
final result = converter(typedElement.exampleValue);
assert(result == typedElement.exampleValue);
print('Note that we have access to the real T: $T');
});
}
} |
I think we can close this issue because it's all about topics that have been raised elsewhere. Do vote for #59050, though, such that we can finally get a compile-time heads up when someone uses one of those "contravariant members"! It's never going to happen if only 4 people in the universe cares about it. ;-) |
Ah, there was one more question that I overlooked: You can't use an extension type because they are using the same subtype checks on functions as any other part of the language (so you can't "hide a soundness violation" inside an extension type). It is possible to use a wrapper class like class FunctionWrapper<TOut, TIn> {
final TOut Function(TIn value) _function;
const FunctionWrapper(this._function);
TOut call(TIn value) => _function(value);
}
void main() {
FunctionWrapper<void, num> w = FunctionWrapper<void, int>((int i) {});
w.call(1.5); // Throws.
} |
the following code:
throws the following error:
what's actually funny is if I run in debug mode, and type the same statement in debug console, it outputs the correct value:
then when going to the next line it throws:
dart info
)The text was updated successfully, but these errors were encountered: