-
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
Function generic type parameters are really inconvenient to extend/cast #3007
Comments
Function types in Dart are invariant in the bounds of type parameters. I believe the main reason for this is that type inference becomes intractable if we allow them to be covariant. @leafpetersen would have more detailed information about this topic. In order to handle the type parameter bounds as invariant, we could use the following emulation of invariance (the extra type parameter typedef UseFunction<T, R> = R Function<S extends T>();
class _BoxedType<T, Invariance> {
R use<R>(UseFunction<T, R> callback) => callback<T>();
}
typedef _Inv<X> = X Function(X);
typedef BoxedType<X> = _BoxedType<X, _Inv<X>>;
/* -= Usage =- */
void main() {
final types = [
BoxedType<int>(),
BoxedType<double>(),
];
R? resolve<R, T>(UseFunction<T, R> callback) {
for (final type in types.whereType<BoxedType<T>>()) {
return type.use(<S extends T>() => callback<S>());
}
print('No type found');
}
print(resolve<Type, int>(<T extends int>() => T)); // Prints 'int'.
print(resolve<Type, num>(<T extends num>() => T)); // Prints 'No type found'.
} The result is that the code is safe (because the invariance property is enforced where it is needed), but also that you can't expect a |
@eernstg When resolving for In my original situation, this is used for a mapper. So instead of:
How would I be able to get this? print(resolve<Type, int>(<T extends int>() => T)); // Prints 'int'.
print(resolve<Type, num>(<T extends num>() => T)); // Prints 'int'. |
But, with invariance, it's simply not true that Without invariance, you do have those subtype relationships, but in that case the member declaration The member signature of |
I forgot one thing: If you are willing to drop the bound of the type parameter entirely then you can get the covariant typing without difficulties, and without unavoidable run-time type errors: typedef UseFunction<R> = R Function<S>();
class BoxedType<T> {
R use<R>(UseFunction<R> callback) => callback<T>();
}
/* -= Usage =- */
void main() {
final types = [
BoxedType<int>(),
BoxedType<double>(),
];
R resolve<R, T>(UseFunction<R> callback) {
for (final type in types.whereType<BoxedType<T>>()) {
return type.use(<S>() => callback<S>());
}
throw Exception('No type found');
}
print(resolve<Type, num>(<T>() => T)); // Prints 'int'.
} This means that the body of the given |
@eernstg That was the first thing I tried to do, but I do need it to have a bound. That's where having an If I can't have a bound at the resolver, I wish I could lower the bound later, like this. class MyNumberValue<T extends num> {} print(
resolve<MyNumberValue, num>(<T>() {
if (T extends num) return MyNumberValue<T>();
throw Exception('Invalid type');
// It's never supposed to throw because the `BoxedType<T>` list was filtered to only have subtypes of `num`
})
); // Prints 'MyNumberValue<int>' Right now, I'm not sure if it even is possible to lower the bound. How would you lower |
In a situation like this where you know, but can't get the type system to understand, that typedef UseFunction<R> = R Function<S>();
class BoxedType<T> {
R use<R>(UseFunction<R> callback) => callback<T>();
}
/* -= Usage =- */
class MyNumberValue<T extends num> {}
void main() {
final types = [
BoxedType<int>(),
BoxedType<double>(),
];
R resolve<R, T>(UseFunction<R> callback) {
for (final type in types.whereType<BoxedType<T>>()) {
return type.use(<S>() => callback<S>());
}
throw Exception('No type found');
}
print(
resolve<MyNumberValue, num>(<T>() {
if (<T>[] is List<num>) {
Function constructor = MyNumberValue.new;
return constructor<T>();
}
// It's never supposed to throw because the `BoxedType<T>`
// list was filtered to only have subtypes of `num`
throw Exception('Invalid type');
})
); // Prints 'MyNumberValue<int>'
} |
This is an interesting problem, I just took one more look at the potential strategies for making this design type safe. ;-) typedef UseFunction<R, B> = R Function<X extends B>();
class BoxedType<X> {
R use<R, B>(UseFunction<R, B> callback) => (callback as Function)<X>();
}
/* -= Usage =- */
void main() {
final types = [
BoxedType<int>(),
BoxedType<double>(),
];
R? resolve<R, B>(UseFunction<R, B> callback) {
for (final type in types.whereType<BoxedType<B>>()) {
return type.use(callback);
}
print('No type found');
}
// The first one that fits is chosen:
print(resolve<Type, int>(<X extends int>() => X)); // Prints 'int'.
print(resolve<Type, num>(<X extends num>() => X)); // Prints 'int'.
print(resolve<Type, Object>(<X extends Object>() => X)); // Prints 'int'.
print(resolve<Type, double>(<X extends double>() => X)); // Prints 'double'.
// We can of course ask for a type that isn't handled.
resolve<Type, String>(<X extends String>() => X); // Prints 'No type found'.
// But `BoxedType.use` isn't statically safe.
BoxedType<int>().use(<X extends String>() => X); // Throws.
} In this variant, we allow the The point is that we can get rid of that last bit of unsafety! Cf. #1674, where I'm proposing that we can use If we can do that then we can change the above example as follows: class BoxedType<X> {
R use<R, B super X>(UseFunction<R, B> callback) => callback<X>();
} It is now statically safe to call Voila, no dynamic calls! |
Problem
When creating a function, it's really inconvenient to use
extends
for generic type parameters, mostly because it's always invariant.When having a function type that takes an
int
as a type parameter, it's not possible to give a more ambiguous type parameter, likenum
, which would handle bothint
anddouble
types.<S extends num>()
be used to handle<S extends int>()
, sincenum
will always handle the same cases asint
?Cheap Workaround
This invariant behavior seems to be more of an inconvenience rather than an impossibility, since you can have a middleman function that casts the type parameter to the target type.
But this doesn't work with my use case.
Simplified Goal
The goal would be to have an handler which can handle any subtype of
num
. This means withT extends num
, it should be able to handleint
anddouble
types.New feature solution
To keep it both compile and runtime safe, it would be nice to have an
extends
condition.This is similar to the following famous workaround, but it would actually change
T
during compile time.Practical Example
This becomes a problem in this following case, because we don't have enough information to create a middleman function to cast.
Even thought there's no compile-time error, the function will throw an exception at runtime.
Even thought we proved that
.whereType<BoxedType<T>>()
is the same T in.use(<S extends T>() => ...)
, and it makes sense because<S extends num>() => ...
can technically handle aBoxedType<int>
, it doesn't work because the only allowed type parameter in.use()
isT extends int
. Throwing a runtime exception.I don't know how to work around the situation above.
Environment
The text was updated successfully, but these errors were encountered: