Coder Social home page Coder Social logo

Comments (1)

eernstg avatar eernstg commented on September 21, 2024 1

Check out this issue about "contravariant members".

The core issue here is that Fn<T> is contravariant in T (e.g., Fn<num> is a subtype of Fn<int>, not vice versa), and Fn<T> is the return type of the getter done in the declaration of A<T>.

This means that T occurs in a non-covariant position in the signature of a member of A<T>, which is exactly what it takes to get the behavior that you're reporting.

Dart uses dynamic type checks to enforce soundness of the heap (that is, roughly: no variable has a value that contradicts its declared type), and these "contravariant members" are particularly prone to cause failures in such a run-time type check.

(You can vote for dart-lang/linter#4111 if you wish to help getting support for detecting when a member is contravariant in this sense, such that you can make sure you don't have any of them.)

Here is a minimal example showing such a run-time failure:

class A<X> {
  void Function(X) fun;
  A(this.fun);
}

void main() {
  A<num> a = A<int>((int i) { print(i.isEven); });
  a.fun; // Throws. We don't even have to call it!
}

However, if you insist that you want to have a contravariant member then you can still make it statically type safe. This means that we get a compile-time error at the location where the situation is created that causes the covariance problem to arise.

This approach relies on a feature which is still experimental, so you need to provide an option when running tools:

// Use option `--enable-experiment=variance`.

void main() {
  A<Object?> a = B<Data>(); // Compile-time error!
  a.done(Data());
}

typedef Fn<T> = void Function(T a);

abstract class A<inout T> {
  Fn<T> get done;
}

class B<inout T> extends A<T> {
  B();

  @override
  Fn<T> get done => (T a) => print(a);
}

class Data {}

The compile-time error in main ensures that we won't have a reference to an object of type B<Data> whose static type is A<Object?>. The reason for this is that B<Data> is simply not a subtype of A<Object?> any more (because of the modifier inout on the type variables).

You can change the declaration to A<Data> a = B<Data>(); which is accepted with no errors, and then you won't have the run-time error.

However, as you can see, you also have to give up on the ability to forget that the actual type argument is Data, which is exactly the point: In order to make the invocation of a.done(...) type safe, you must remember that this function needs an argument of type Data, and if you're allowed to think that any Object? will do then there is no way we can avoid performing the type check at run time and potentially have the run-time failure.

You mention that the following variant is more forgiving:

void main() {
  A<Object?> a = B<Data>();
  a.done(Data()); // No compile-time error, succeeds at run time.
  a.done(false); // No compile-time error, throws at run time.
}

typedef Fn<T> = void Function(T a);

abstract class A<T> {
  void done(T a);
}

class B<T> extends A<T> {
  B();

  @override
  void done(T a) =>  print(a);
}

class Data {}

The reason why this variant will run successfully (until we reach a.done(false), at least!) is that it does not obtain an object which is mistyped at any point in time (that is, we maintain soundness at all times).

In particular, we can check dynamically that the argument passed to a.done(Data()) has the required type (it has type Data, as required by the actual value of a), and then we can proceed without throwing.

At a.done(false) we perform the same dynamic type check, but it fails and the invocation throws.

In contrast, the expression a.done in the original version of this example evaluated to obtain a function object of type void Function(Data), but the static type of a.done was void Function(Object?). That's a soundness violation because void Function(Data) is not a subtype of void Function(Object?). When we have a soundness violation we will have a run-time type error, period.

So you never take the next step and try to call that function object. So it doesn't help that you might pass an argument like Data() that would have satisfied the function object, because we don't even try to call the function object in the situation where the function object itself is "bad".

Note that you can combine the two approaches if it is important for you to use separate function objects rather than instance methods:

void main() {
  A<Object?> a = B<Data>();
  a.done(Data());
}

typedef Fn<T> = void Function(T a);

abstract class A<T> {
  Fn<T> get _done;
  void done(T a) => _done(a);
}

class B<T> extends A<T> {
  B();

  @override
  Fn<T> get _done => (T a) => print(a);
}

class Data {}

The invocation of _done(a) will take place because the static type of _done inside the body of the class is void Function(T), and there's nothing unsafe about evaluating _done in this context. (In other words, no member is "contravariant" when used from inside the class itself). The dynamic type check will now take place at the invocation of the instance method done, and the invocation of the function object relies on the fact that the type has already been checked, and it just works. The price you'll have to pay in order to get "the best of both words" is that it is more costly at run time to call two functions than it is to call just one; YMMV.

I'll close this issue because it is all working as specified.

from sdk.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.