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

[Type Error] (int) => void is not a subtype of type (dynamic) => void #53523

Closed
gintominto5329 opened this issue Sep 14, 2023 · 2 comments
Closed
Labels
closed-as-intended Closed as the reported issue is expected behavior type-question A question about expected behavior or functionality

Comments

@gintominto5329
Copy link

hello,

We need to know, whenever some functions are called, along with the passed arguments.

Note: Whole testing is done, on Dartpad, with Master channel

The following error, is being output, by then following code

Error

TypeError: Closure 'register_int': type '(int) => void' is not a subtype of type '(dynamic) => void'

Complete output

fxn called, with args
( 
  arg: 
    
: TypeError: Closure 'register_int': type '(int) => void' is not a subtype of type '(dynamic) => void'Error: TypeError: Closure 'register_int': type '(int) => void' is not a subtype of type '(dynamic) => void'

Code

class function_arg<T> {
  const function_arg(
    this.id,
    this.register,
    this.value,
  );

  final String id;
  final void Function(T) register;
  final T value;
}

void register_function_call(
  final String id,
  final List<function_arg<dynamic>> args,
) {
  print("$id called, with args");

  print("( ");

  args.forEach((final arg) {
    print("  " + arg.id + ": \n    ");

    arg.register(arg.value);

    print(", ");
  });

  print(")\n");
}

void register_int(final int i) => //
    print("'" + i.toString() + "'");

void register_string(final String string) => //
    print('"' + string + '"');

void main() => //
    register_function_call(
      "fxn",
      [
        function_arg<int>("arg", register_int, (123)),
        function_arg<String>("anotherArg", register_string, ("abc, " * 2)),
      ],
    );

Code notes

  • For arguments, Record type is un-suitable, because of its alphabetic sorting of members, we need them in specified order, otherwise its perfect.
  • prints are just for reproduction of the issue, please do not become stack-overflow, regarding best practices

thanks

@mraleph
Copy link
Member

mraleph commented Sep 14, 2023

This is a corner case of how function types interact with covariance in Dart. Consider

function_arg<dynamic> arg = function_arg<int>("arg", (int v) { }, 123);

arg.register("whatever");

arg.register has static type void Function(dynamic) - but the underlying closure can't actually handle arbitrary arguments, it only expects int. In general we expect closure calls to not require implicit type-checks of arguments. So to prevent unsoundness but still maintain this property (that closure calls don't require type checking on entry) Dart chooses to enforce consistency between static and runtime type of arg.register by injecting implicit check (arg.register as void Function(dynamic)) - that's where you get this runtime error from.

To work around the issue you need to shift invocation to a context where type is reified, e.g.

class function_arg<T> {
  void invoke() { register(value); }
}

@mraleph mraleph closed this as completed Sep 14, 2023
@mraleph mraleph added closed-as-intended Closed as the reported issue is expected behavior type-question A question about expected behavior or functionality labels Sep 14, 2023
@eernstg
Copy link
Member

eernstg commented Sep 15, 2023

Agreeing with @mraleph's explanation, I'd like to mention a broader perspective on this program:

Introducing a method like invoke is a very good solution because this makes the invocation of register(value) statically safe.

However, the declaration of register in function_arg is a "contravariant member" declaration, as described in dart-lang/language#296, and that declaration is dangerous in its own right. You can make it private, but it is still unsafe to use register at all (you don't even have to call it in register_function_call it's enough to just evaluate the expression arg.register, even if you ignore the returned result).

So if you really need to have an instance variable whose type is void Function(T) where T is a type parameter of the class then you need to make T invariant.

We do not currently have direct support for that. However, we could change the language to support it by adding support for declaration-site variance, dart-lang/language#524. (@gintominto5329, if you want to support this feature, you can vote for that issue).


If you're interested, here is a brief explanation about how that would work. With declaration-site variance, we could declare function_arg as follows:

// Assuming `--enable-experiment=variance`.

class function_arg<inout T> { // <-- `T` is changed to `inout T`. No other changes needed.
  const function_arg(
    this.id,
    this.register,
    this.value,
  );

  final String id;
  final void Function(T) register;
  final T value;
}

The modifier inout on the type parameter T makes that type parameter invariant. This is needed because we are using T both as the return type of a getter (a covariant position) and as the parameter of a function (a contravariant position). Having added this modifier, we have eliminated the subtype relationships that were causing the run-time error: function_arg<int> is no longer a subtype of function_arg<dynamic>.

This implies that we get a compile-time error at the location where we previously laid the foundation for the run-time error:

Analyzing n018.dart...                 2.0s

  error • n018.dart:42:9 • The element type 'function_arg<int>' can't be
          assigned to the list type 'function_arg<dynamic>'. •
          list_element_type_not_assignable
  error • n018.dart:43:9 • The element type 'function_arg<String>' can't be
          assigned to the list type 'function_arg<dynamic>'. •
          list_element_type_not_assignable

2 issues found.

If you want to get this kind of type checking now, where declaration-site variance hasn't yet been added to the language, you can emulate it as follows:

typedef function_arg<T> = _function_arg<T, Function(T)>;

class _function_arg<T, Invariance> {
  const _function_arg(
    this.id,
    this.register,
    this.value,
  );

  final String id;
  final void Function(T) register;
  final T value;
}

This implies that you cannot have a function_arg<int> and a function_arg<String> in a list of type List<function_arg<S>> no matter which type you are using as S, but that was an unsafe setup anyway as long as clients can access register at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-as-intended Closed as the reported issue is expected behavior type-question A question about expected behavior or functionality
Projects
None yet
Development

No branches or pull requests

3 participants