-
Notifications
You must be signed in to change notification settings - Fork 213
Type inference does not solve some constraints involving F-bounds #3009
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
[Edit: Eliminated a couple of mistakes.] Here's an outline of an approach that we could explore: In section Constraint solution for a set of type variables we have the following step:
We could enhance this step such that it works differently when In that case we would not find the constraint solution as described. We would instead consider For each occurrence of Check that Otherwise, If any of the steps up to this point fails then we fall back and use the approach which is used today. The point is that the binding of Similarly, in section Grounded constraint solution for a set of type variables we have the following, and it should be enhanced to handle F-bounds similarly:
|
@stereotype441, here's the issue I mentioned at breakfast. ;-) |
An extra note on known solutions: Assume that In this situation, In the case where every occurrence of If every occurrence of Based on these considerations, I tend to conclude that the proposed algorithm here can ignore these solutions, using only the nominally declared solutions (like using |
We're currently exploring a solution to this issue, based on a generalization of the type inference algorithm that introduces a few extra subtype constraints on the type variables which are being computed, based on a more intensive use of the type parameter bounds. As a consequence, we will be able to infer the following: class A<X extends Iterable<Y>, Y> {
A(X x) {
print('A<$X, $Y>');
}
}
void main() {
A([1]); // 'A<List<int>, dynamic>' today, becomes 'A<List<int>, int>'.
} The reason for this change is that the new type inference algorithm generates a constraint that |
I want to bring this package to your attention: https://pub.dev/documentation/dart_internal/latest/extract_type_arguments/extract_type_arguments-library.html There are two pub packages that depend on it: At first glance, it sounds like this change could allow the owners of these packages to rewrite their APIs to remove their dependency on package:dart_internal and unblock backends from eventually dropping support for it. Is that use case on your radar? |
Does this mean that #1674 would be related to this as well? I'm not sure it would do everything that issue is asking, but it seems pretty close to me. |
The addition of support for declared lower bounds on type parameters (that is, #1674) isn't quite the same as this feature. A typical example of #1674 would be that we wish to specify a type at a call site which is a locally known supertype of a type parameter of the receiver: import 'dart:math';
abstract class A<X> {
Y or<Y super X>(Y y);
}
class B1<X> implements A<X> {
final X x;
B1(this.x);
Y or<Y super X>(Y y) => x; // <--- This body needs the `super` bound.
}
class B2<X> extends A<X> {
Y or<Y super X>(Y y) => y;
}
void main() {
A<num> a = Random().nextBool() ? B1(20) : B2();
num n = a.or(2.5);
} In order to make At the call site for |
@nshahan wrote:
Those two features are not quite the same: The improved type inference which is discussed in this issue is a static analysis, and the 'extract_type_arguments' library provides a feature that provides access to the run-time values of type parameters. In particular, we can't do the latter with the former. On the other hand, there's no need to use external functions (as 'extract_type_arguments' does) when considering the task of extracting the values of type parameters in isolation, it is only done in that way because it would be too much of a breaking change to add a method to class C<X> {
X x;
C(this.x);
R callWithX<R>(R Function<Y>(C<Y> self) callback) => callback<X>(this);
}
void main() {
C<Object> c = C<String>('Hello'); // "Forget" the actual type argument.
c.callWithX(<X>(C<X> _) => print(X)); // 'String'.
} You may or may not prefer to pass the receiver to the callback, it's just done here in order to ensure that the body of the callback has access to the receiver with the best possible typing. If you don't do that then the body can always do something like |
Ah yes thinking about it more now that makes perfect sense, thanks for pointing it out. |
One more thing that I'd like to ask in relation to your comment. The folowing works just fine (see that all are TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: 0, end: -90), weight: 1),
TweenSequenceItem(tween: Tween(begin: -90, end: 0), weight: 1),
]) But when I add TweenSequence<double>([
TweenSequenceItem(
tween: Tween(begin: 0, end: -90).chain(CurveTween(curve: Curves.easeInQuad)), // <--------- here
weight: 1
),
TweenSequenceItem(tween: Tween(begin: -90, end: 0), weight: 1),
]) The only thing added was: I then get: In the whole expression: Is this intended behaviour? Would your new approach solve this case? Or even, should I file an issue with this? |
This is indeed behaving as it is expected to behave. It is a part of type inference which is being explored actively, see #3527, and we might have a substantial improvement in the future. It will take some time, though, because it is rather complex. The basic issue here is that the receiver of a member access expression (e.g., a method call) will have the empty context type during type inference, and this means that the receiver expression will obtain the type which is based on its own subexpressions. For example: extension<X> on X {
X get id => this;
}
void main() {
// For an integer literal, the context type can make it an `int` or a `double`.
double d = 1; // `1` evaluates to the `double` value `1.0` at run time.
var v = 1; // `1` evaluates to the `int` value `1` at run time.
d = v; // Compile-time error.
// As a receiver, `1` always evaluates to an `int`.
d = 1.id; // Error, `1.id` has the type `int`.
} One way to say this is that we can't transfer the context type from a member access to the receiver: If we're considering Similarly, when we consider In this particular case the receiver is However, what we're basically considering here is the set of possible output types from type inference (like: "tell me every possible type of the result of performing type inference on X whatever<X>() => throw "Haha!";
void main() {
SomeContextType x = whatever().chain(CurveTween(curve: Curves.easeInQuad));
} In this case the type of the receiver could actually be every single denotable type in the universe (or at least the ones that we've imported ;-). In summary, it isn't a simple matter to "go backwards" through a member invocation and find the corresponding receiver type, because there's no limit on the number of completely unrelated types out there which do have a member with that particular name, and some of them might have a return type (after yet another round of type inference) which is assignable to the original context type. In any case, @chloestefantsova is able to do something about this, which is obviously pure magic. 😄 But it will take a while before we can use it. |
The change which is performed in https://dart-review.googlesource.com/c/sdk/+/364721 has been accepted by the language team, and it will be landed soon. 🎉 |
In response to dart-lang/language#3009 Change-Id: I918d392e422f1997736bf2543c8107cb44d3d6f8 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/364721 Reviewed-by: Erik Ernst <[email protected]> Reviewed-by: Johnni Winther <[email protected]> Reviewed-by: Michael Thomsen <[email protected]> Reviewed-by: Paul Berry <[email protected]>
Landed in https://dart-review.googlesource.com/c/sdk/+/364721. 🎉 The feature is currently behind the flag |
Thank you @eernstg for this solution! We are starting to use this experimental flag in one of our projects now. Don't know if this is of use but we had a somewhat differently shaped use-case where this came up: abstract interface class FactoryInterface<R> {
R construct();
}
class FactoryImpl implements FactoryInterface<String> {
@override
String construct() => 'foo';
}
class Retainer<F extends FactoryInterface<R>, R> {
final F _factory;
R? _resource;
Retainer(this._factory);
F get getFactory => _factory;
R get getResource {
_resource ??= _factory.construct();
return _resource!;
}
}
void main() {
final myFactory = FactoryImpl();
final retainer = Retainer(myFactory);
final f = retainer.getFactory; // f has type FactoryImpl - correct
final r = retainer.getResource; // r has type dynamic - should be String
print(r);
} A question - in which Dart release may this become a proper (non-experimental) feature? |
@christerswahn I just tested your example at Dart 3.8.0-edge.49a9d42cfc08138b3d230296d704ae99c4ae1323, this is working as expected. The hover for |
Thanks for the kind words, @christerswahn! @FMorschel, you are right, the feature may or may not be enabled right now, depending on the setup. It is enabled in Dart 3.7.0. DartPad 'Main channel' can be used to try it out with no 'experiment' flags. |
@FMorschel We are using Dart 3.6.2. With this experimental flag the hover shows the correct String type for me as well! Without that flag it shows dynamic. |
@christerswahn this is expected. You'll need the flag if you are using stable for now until a new stable release for Dart 3.7.0 or above is out, or you can use the Beta/Main channels if you really want to, but I'd keep the flag since this is simpler. |
Consider the following program:
Most likely, the inference could succeed in choosing the type argument
B
even in the case where the actual argument has typeC
if the bounds (in this case: an F-boundX extends A<X>
) are taken into account during constraint solution. In any case, the current behavior is that inference fails as mentioned, and this is a problem that does occur in practice (e.g., dart-lang/sdk#35799).The text was updated successfully, but these errors were encountered: