-
Notifications
You must be signed in to change notification settings - Fork 205
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
Unwanted Object
-inference induced type errors.
#3156
Comments
It sounds like what you really need here is a way to disable the covariance, so that a In particular, the variance proposal is I believe what you want, so you would define class Foo<in T> {...} This would make Foo be contravariant instead of covariant for its generics. This article has a decent introduction https://medium.com/dartlang/dart-declaration-site-variance-5c0e9c5f18a5. |
Thank you, Jake. Should sound variance land, and if it will be able to help with completely avoiding the pitfalls related to this issue, then I think that that would be great and definitely a solution. However, I still think that it feels a little unexpected, even without type errors, that, for example, in the following example: void main() {
bar(
data1: 123,
data2: "data",
);
}
void bar<T>({
required final T data1,
required final T data2,
}) {
print(T);
}
|
This problem really is not constraint to For instance if you consider your original example, and change dataSource to It is true that I could see a special lint or something (see the linter repo) that warned you when LUB resulted in |
You might want to emulate invariance if you need anything other than covariance (invariance is the most strict requirement, which means that it subsumes contravariance): void main() {
// int dataSource() => 123;
String dataSource() => "data";
Foo<int> fooSource() => Foo<int>();
bar(
foo: fooSource(), // Compile-time error. OK when using the other `dataSource`.
data: dataSource(),
);
}
void bar<T>({
required final Foo<T> foo,
required final T data,
}) {
print(T);
foo.baz(data);
}
// Foo stuff, perhaps in a different library (such that privacy matters).
typedef _Inv<X> = X Function(X);
typedef Foo<X> = _Foo<X, _Inv<X>>;
class _Foo<T, Invariance> {
void baz(T t) {}
} (and it's also a compile-time error to use |
Thank you, Erik. This seems like it indeed solves the issue here. Unfortunately, I don't think that it would be practical for me to go and update all my code to use this technique. So it looks like I'll have to wait for #524. |
My understanding is that lints have somewhat looser correctness guarantees. There are some analyses that the language team appears to maintain. I'm not sure if there are plans to update them (especially because this would potentially be a breaking change), but:
I think it would be great if any of those analyses (or a new one) could include this rule (or something similar that would help with this issue). |
You did notice that the declaration of |
Yes, and I think that this trick is pretty cool. However, it still requires at least an extra declaration per declaration. It might not seem like a lot, but if I were to update thousands of declarations, just to use this trick (which I would need to do), that would be extremely tedious. |
Sorry about pushing on this one again, but I'm curious: Given that we have had dynamically checked covariance for more than a decade, and the number of people who are really enthusiastic about adding statically checked declaration-site variance is rather small, it looks like the dynamic errors occur in specialized situations. The use of a "contravariant member" is an obvious example. Are you sure you'd need to do this with thousands of classes, and not just a handful? |
No, I appreciate that, thank you. It's always a possibility that I have made a mistake somewhere. I try to use the soundness properties of the Dart type system to give me strong guarantees that my programs are statically verified not to have certain classes of errors. The most economical way (that I can think of) to "prove" (with a high degree of certainty) that I have eliminated the class of type errors that this issue deals with, is to attempt to use the technique that you've mentioned on all of my generic declarations, and to see which ones need it or not.
That could be the case, but I don't want to rely on my intuition here because It can make mistakes. I would like to eventually be able to assume that errors from this class of errors will never appear again.
I've translated the Dart program (that throws at runtime) to Java, Kotlin, Swift, Scala, TypeScript and C++ and they all reject that program at compile-time. Maybe people are just not aware that this is an issue or what the issue actually is? People also don't seem to be enthusiastic about seat belts, street lamps or hospitals, they just take them for granted, so I don't think that upvotes or the amount of excitement would necessarily correlate with how important a language feature is. Java rejects this program: public class Main {
public static void main(String[] args) {
bar(new Foo<Integer>(), "data");
}
public static <T> void bar(Foo<T> foo, T data) {}
public static class Foo<T> {
public void baz(T t) {
}
}
}
Swift rejects this program: bar(foo: Foo<Int>(), data: "data")
func bar<T>(foo: Foo<T>, data: T) {
print(T.self)
foo.baz(data: data)
}
class Foo<T> {
func baz(data: T) {}
}
Kotlin rejects this program: fun main() {
bar(Foo<Int>(), "data")
}
fun <T> bar(foo: Foo<T>, data: T) {}
class Foo<T> {
fun baz(t: T) {}
}
Scala rejects this program: object Main {
def main(args: Array[String]): Unit = {
bar(new Foo[Int](), "data")
}
def bar[T](foo: Foo[T], data: T): Unit = {}
class Foo[T] {
def baz(t: T): Unit = {}
}
}
TypeScript rejects this program: function main(): void {
bar(new Foo<number>(), "data");
}
function bar<T>(foo: Foo<T>, data: T): void {}
class Foo<T> {
baz(t: T): void {
}
}
C++ rejects this program: #include <iostream>
template<typename T>
class Foo {
public:
void baz(T t) {
}
};
template<typename T>
void bar(Foo<T> foo, T data) {}
int main() {
bar(Foo<int>(), "data");
return 0;
}
|
Afaik know, what you need to look for is any class which has a method with a parameter whose type contains a generic type parameter from the class. This means it takes something of the generic type "in" as an argument, which means it can't be safely treated as covariant. |
I agree that that might work very well for cases like the one in the issue description. However, consider, for example, when Foo is part of a nested structure (such as an AST): FooFriendFriendFriendvoid main() {
bar(
foo: FooFriendFriendFriend<int>(),
data: "data",
);
}
void bar<T>({
required final FooFriendFriendFriend<T> foo,
required final T data,
}) {
print(T);
foo.foo.foo.foo.baz(data);
}
class FooFriendFriendFriend<T> {
FooFriendFriend<T> foo = FooFriendFriend();
}
class FooFriendFriend<T> {
FooFriend<T> foo = FooFriend();
}
class FooFriend<T> {
Foo<T> foo = Foo();
}
typedef _Inv<X> = X Function(X);
typedef Foo<X> = _Foo<X, _Inv<X>>;
class _Foo<T, _$I> {
void baz(
final T t,
) {}
}
Running the program above throws:
That is, I think, we would have to make FooFriend, FooFriendFriend, FooFriendFriendFriend and everybody else that "stores" any of them also use that technique to guarantee safety. Of course, this is doable, but I think this would be a lot of work. |
@modulovalue wrote:
Right, one way to do that would be to However, I suspect that the lowest hanging fruit is to make a type variable invariant if it is used in a non-covariant position in a member signature which is not the type of a formal parameter. That is, typically, when a type variable is used in the type of an instance variable or in a return type which is a function type: class C<X> {
final void Function(X) f; // Danger! "Contravariant member".
...
}
That's possible. It is also possible that the danger may be considered acceptable (and the issue about dynamically checked variance in general isn't being debated so widely) because certain elements of programming style tend to make these errors infrequent. For instance, if lists (or class C<X> {
final _list1 = <X>[], _list2 = <X>[];
final X _x;
C(this._x);
void baz() {
_list1.add(x);
if (_list2.isNotEmpty) _list1.add(_list2.first);
}
} Invocations of I'm obviously in favor of introducing the statically checked kind of variance, but I must also admit that the dynamically checked covariance has been working much more smoothly in practice than I had expected. Considering the "friend" example that causes a run-time type error, here's a variant that emulates invariance: void main() {
bar(
foo: FooFriendFriendFriend<int>(),
data: "data", // Compile-time error. An `int` is OK.
);
}
void bar<T>({
required final FooFriendFriendFriend<T> foo,
required final T data,
}) {
print(T);
foo.foo.foo.foo.baz(data);
}
typedef FooFriendFriendFriend<X> = _FooFriendFriendFriend<X, _Inv<X>>;
class _FooFriendFriendFriend<T, _$I> {
var foo = FooFriendFriend<T>();
}
typedef FooFriendFriend<X> = _FooFriendFriend<X, _Inv<X>>;
class _FooFriendFriend<T, _$I> {
var foo = FooFriend();
}
typedef FooFriend<X> = _FooFriend<X, _Inv<X>>;
class _FooFriend<T, _$I> {
Foo<T> foo = Foo();
}
typedef Foo<X> = _Foo<X, _Inv<X>>;
class _Foo<T, _$I> {
void baz(T t) {}
}
typedef _Inv<X> = X Function(X); Granted, it's ugly! ;-) However, note that it is necessary to mark the type variable as Perhaps it's good that it is so ugly: If people start doing this a bit more often, they might vote for #524. ;-) |
@eernstg I agree with everything you said, and thank you and @jakemac53 for your help. So to conclude:
|
@modulovalue, perhaps this issue should be closed at this time? I think the behavior of the tools in the current language is as expected, and language enhancements that would address the issues described here would amount to #524 (and possibly some other proposals that are similar). |
@eernstg I think so, too. |
Recently, I was surprised by unexpected type errors at runtime.
They seem to have been introduced when the return type of a function was migrated to a different type.
Consider the following:
Notice how a dataSource that returns a
String
causes theT
in bar to be anObject
(foo.baz throws), and a dataSource that returns anint
causes theT
in bar to anint
(foo.baz does not throw).I can't think of many practical situations where I'd want
T
of bar to be inferred to anObject
. And in the rare cases whereObject
might be wanted there, I'd much rather prefer to be explicit about the type argument and specifyT
to beObject
myself.One way to prevent this would be to be more explicit and always specify all type arguments. However, I'd prefer not to do that because that would be really verbose.
Could something like
implicit-dynamic
(e.g.implicit-object
) help here? (or could maybe strict-inference be extended to help prevent errors like these?)The text was updated successfully, but these errors were encountered: