Skip to content
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

Inconsistent anonymous function type checking #2055

Closed
tschai-yim opened this issue Jan 10, 2022 · 3 comments
Closed

Inconsistent anonymous function type checking #2055

tschai-yim opened this issue Jan 10, 2022 · 3 comments
Labels
bug There is a mistake in the language specification or in an active document

Comments

@tschai-yim
Copy link

Hello, I found an inconsistency with the type checking of lambdas. I'm guessing this has more to do with how Dart's type system works and not a bug in the implementation so I'll file the issue here.

Problem

When assigning a value of type Function(type) (type can be any type other than dynamic) to a variable of type Function(dynamic) it fails.

But when you wrap the above lambda in a class the assignment works but you are unable to use it. Furthermore, when it is a method instead of a lambda property the usage works again.

class SomeClass<TGeneric> {
  void Function(TGeneric) someProperty;

  SomeClass(this.someProperty);

  void someMethod(TGeneric a) {}
}

void main() async {
  /* Fails:
   * void Function(dynamic) foo = (String a) {};
   * foo('hello');
   * 
   * Result:
   * Error: A value of type 'void Function(String)' can't be assigned to a variable of type 'void Function(dynamic)'.
   */

  // Works
  SomeClass bar = SomeClass<String>((a) {});

  // Works
  bar.someMethod('hello');

  // Fails
  bar.someProperty('hello');
  // Uncaught Error: TypeError: Closure 'main_closure': type '(String) => void' is not a subtype of type '(dynamic) => void'
}

Possible solutions

Allow Function(type) to Function(dynamic) assignment

TypeScript does it this way:

class SomeClass<TGeneric> {
    someProperty: ((a: TGeneric) => void);

    constructor(someProperty: (a: TGeneric) => void) {
        this.someProperty = someProperty
    }

    someMethod(a: TGeneric): void { }
}

// Works
const foo: (a: any) => void = (a: string) => { };
foo('hello');

// Works
const bar: SomeClass<any> = new SomeClass<string>((a) => { });
bar.someMethod('hello');
// Works
bar.someProperty('hello');

Disallow Function(type) to Function(dynamic) assignment and properly check the class properties

Scala does it this way:

class SomeClass[TGeneric](var someProperty: (a: TGeneric) => Unit) {
  def someMethod(a: TGeneric): Unit = {}
}

// Fails
val foo: (a: Any) => Unit = (a: String) => {};
// Error:
// Found:    String => Unit
// Required: (Any) => Unit
foo("hello");

// Fails
val bar: SomeClass[Any] = new SomeClass[String]((a) => {});
// Error:
// Found:    Playground.SomeClass[String]
// Required: Playground.SomeClass[Any]
bar.someMethod("hello");
bar.someProperty("hello");
@tschai-yim tschai-yim added the bug There is a mistake in the language specification or in an active document label Jan 10, 2022
@eernstg
Copy link
Member

eernstg commented Jan 10, 2022

You would naturally want contravariance (or invariance) in order to have an instance variable where a type variable declared by the enclosing class occurs in the type in a contravariant position:

// Needs declaration-site variance support.
// Experimental support: `--enable-experiment=variance`. NB: Not released, not complete.

class SomeClass<in TGeneric> { // Or `inout TGeneric`, for invariance.
  void Function(TGeneric) someProperty;

  SomeClass(this.someProperty);

  void someMethod(TGeneric a) {}
}

Cf. #524 for more information about declaration-site variance. With this declaration you'd get the traditional strict type checking:

void main() async {
  // `dynamic <: String` required, but does not hold.
  void Function(dynamic) foo = (String a) {}; // Error.
  foo('hello');

  // `SomeClass` means `SomeClass<dynamic>`, and `dynamic <: String` does not hold.
  SomeClass bar = SomeClass<String>((a) {}); // Inferred as `(String a) {}`. // Error

  // Allowed, based on the declared type of `bar`.
  bar.someMethod('hello');

  // Allowed, based on the declared type of `someProperty`.
  bar.someProperty('hello');

  // You'd probably want this instead.
  var baz = SomeClass<String>((a) {}); // Inferred type of `baz` is `SomeClass<String>`.
  baz.someMethod(''); baz.someProperty(''); // OK.
}

Today (where declaration-site variance has not yet been added to the language), and since the language was initiated about a decade ago, Dart uses dynamically checked covariance for all class type variables. This is exactly the way Java and C# have always treated arrays, and they also perform dynamic checks (such as ArrayStoreException in Java). The original design of Dart took that approach further, and used dynamically checked covariance for all type variables, not just the ones associated with arrays.

This means that every type variable is considered to be covariant, and subtype relationships like List<int> <: List<num> exist, and in the cases where a type variable is used in a contravariant position the affected operations include a dynamic type check (such that we never create a heap where some declared types are violated). For example:

void main() {
  List<num> xs = <int>[];
  xs.add(1); // OK statically, dynamic check `1 as int`, succeeds.
  xs.add(1.5); // OK statically, but the dynamic check `1.5 as int` fails, and the invocation throws.
}

In the case where an instance variable has a type where a type variable occurs in a non-covariant position, we may get a dynamic error when the instance variable is evaluated, because it does not have the statically known type:

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

void main() {
  A<num> a = A<int>((i) {});
  print(a.f); // Dynamic error: `a.f` has static type `void Function(num)`, actual type is `void Function(int)`.
}

Cf. #297 as well, for comments/references on "contravariant members".

In Dart of today, hence, you simply don't want to have an instance variable whose type contains a type variable from the enclosing class in a non-covariant position.

@eernstg
Copy link
Member

eernstg commented Jan 10, 2022

Given that Dart currently uses dynamically checked covariance for all type variables, what can you do today?

abstract class SomeClass<X> {
  void someMethod(X a);
}

// Create subclasses and override `someMethod` as needed.

Maybe you'll be able to use the above declaration, if you need to have someMethod, and you don't have to have someProperty.

With that declaration of SomeClass there will be a dynamic type check whenever someMethod is invoked. However, perhaps surprisingly, dynamically checked covariance has turned out to be quite practical as long as it only involves contravariant occurrences of type variables in this particular manner (which is surely vastly more common than all other usages). Sure, you may encounter a dynamic type error in a program that has no compile-time errors, but this is not a problem that dominates the github issue tracker in practice.

Alternatively, you may be able to use the following approach, if you really need to have an instance variable whose type is a function type:

class SomeClass<X extends void Function(Never)> {
  X someProperty;
  SomeClass(this.someProperty);
}

void main() {
  var x = SomeClass((String s) {}); // `x` has type `SomeClass<void Function(String)>`.
  x.someProperty('Hello'); // OK, statically and dynamically.
  x.someProperty = (dynamic d) {}; // OK.
  x.someProperty('world!'); // OK, statically and dynamically.

  // The value of `x.someProperty` does actually accept any argument,
  // and we could use a dynamic invocation to pass non-string arguments.
  (x.someProperty as dynamic)(true); // OK, statically and dynamically.
}

In this case someProperty is simply a normal (not "contravariant") instance variable, and the subtype relationships only involve the normal function type relationships (which are statically safe and hence never involve dynamic type checks). So if you're able to use this, it should work smoothly.

Do note that you will then, of course, get an error in case the statically known type involves dynamic and the actual value involves String:

void main() {
  SomeClass<void Function(dynamic)> x = SomeClass<void Function(String)>((s) {}); // Error.
  // .. but if the declaration of `x` had not had an error in the initializing expression ..
  x.someProperty(true); // OK, based on the declared type of `x`.
}

@eernstg
Copy link
Member

eernstg commented Jan 10, 2022

Basically, this issue reports consequences of very old language design choices that we would handle by adding sound declaration-site variance (plus possibly something like use-site invariance), and those things are already the topic of existing language repo issues (#524 and #297, plus a few others).

In that sense this issue is a duplicate of those language repo issues, and it actually isn't reporting on any behavior that isn't working as intended, so I'll close the issue.

@tschai-yim, please follow those other issues and comment there if needed, or create a new issue proposing a different take on variance if the existing proposals do not fit your ideas at all.

@eernstg eernstg closed this as completed Jan 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug There is a mistake in the language specification or in an active document
Projects
None yet
Development

No branches or pull requests

2 participants