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 in generic closure #42763

Closed
tvolkert opened this issue Jul 19, 2020 · 4 comments
Closed

Type error in generic closure #42763

tvolkert opened this issue Jul 19, 2020 · 4 comments

Comments

@tvolkert
Copy link
Contributor

Steps to reproduce

import 'package:flutter/widgets.dart';

typedef ValueCallback<T> = void Function(T value);

void main() {
  runApp(
    BugReport<String>(
      onValue: (String value) {
        print(value);
      },
    ),
  );
}

class BugReport<T> extends StatefulWidget {
  const BugReport({
    @required this.onValue,
  });

  final ValueCallback<T> onValue;

  @override
  _BugReportState<T> createState() => _BugReportState<T>();
}

class _BugReportState<T> extends State<BugReport> {
  @override
  void initState() {
    super.initState();
    print(widget.onValue);  // <----- This should be fine, but it throws
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Expected behavior

You expect no errors in running this code

Actual behavior

The following error is thrown:

══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following _TypeError was thrown attaching to the render tree:
type '(String) => Null' is not a subtype of type '(dynamic) => void'

When the exception was thrown, this was the stack:
#0      _BugReportState.initState (package:bugreport/main.dart:32:18)
#1      StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4695:58)
#2      ComponentElement.mount (package:flutter/src/widgets/framework.dart:4531:5)
#3      Element.inflateWidget (package:flutter/src/widgets/framework.dart:3501:14)
#4      Element.updateChild (package:flutter/src/widgets/framework.dart:3260:18)
#5      RenderObjectToWidgetElement._rebuild (package:flutter/src/widgets/binding.dart:1217:16)
#6      RenderObjectToWidgetElement.mount (package:flutter/src/widgets/binding.dart:1188:5)
#7      RenderObjectToWidgetAdapter.attachToRenderTree.<anonymous closure> (package:flutter/src/widgets/binding.dart:1130:17)
#8      BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2620:19)
#9      RenderObjectToWidgetAdapter.attachToRenderTree (package:flutter/src/widgets/binding.dart:1129:13)
#10     WidgetsBinding.attachRootWidget (package:flutter/src/widgets/binding.dart:941:7)
#11     WidgetsBinding.scheduleAttachRootWidget.<anonymous closure> (package:flutter/src/widgets/binding.dart:922:7)
(elided 11 frames from class _RawReceivePortImpl, class _Timer, dart:async, and dart:async-patch)
════════════════════════════════════════════════════════════════════════════════════════════════════

Other information

[✓] Flutter (Channel master, 1.21.0-2.0.pre.55, on Mac OS X 10.15.3 19D76, locale en-US)
    • Flutter version 1.21.0-2.0.pre.55 at /Users/tvolkert/project/flutter/flutter
    • Framework revision 1840b7121a (3 days ago), 2020-07-16 23:15:23 -0700
    • Engine revision d3b81f19fc
    • Dart version 2.9.0 (build 2.9.0-21.0.dev 06183779d7)
@tvolkert
Copy link
Contributor Author

Here's a reproducible case not involving Flutter:

typedef ValueCallback<T> = void Function(T value);

void main() {
  BugReport<String> bugReport = BugReport<String>((String value) {
    print(value);
  });
  bugReport.createOtherClass();
}

class BugReport<T> {
  const BugReport(this.onValue);

  final ValueCallback<T> onValue;

  OtherClass<T> createOtherClass() {
    OtherClass<T> result = OtherClass<T>(this);
    result.init();
    return result;
  }
}

class OtherClass<T> {
  OtherClass(this._bugReport);

  BugReport _bugReport;

  void init() {
    print(_bugReport.onValue);
  }
}

Result:

$ dart main.dart 
Unhandled exception:
type '(String) => Null' is not a subtype of type '(dynamic) => void'
#0      OtherClass.init (file:///Users/tvolkert/project/bugreport/main.dart:28:22)
#1      BugReport.createOtherClass (file:///Users/tvolkert/project/bugreport/main.dart:17:12)
#2      main (file:///Users/tvolkert/project/bugreport/main.dart:7:13)
#3      _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:301:19)
#4      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)

@jonahwilliams
Copy link
Contributor

The problems here, in both the Flutter and Dart examples, are that the declarations of BugReport without a specified type parameter are defaulting to BugReport<dynamic>. See also: #33119

- class _BugReportState<T> extends State<BugReport> {
+ class _BugReportState<T> extends State<BugReport<T>> {

}
class OtherClass<T> {
  OtherClass(this._bugReport);

  - BugReport _bugReport;
  + BugReport<T> _bugReport 
 
  void init() {
    print(_bugReport.onValue);
  }
}

Since T is dynamic, this leads to the type of the value callback being inferred as void Function(dynamic) (Null return is unimportant). And since function types are contraviant with respect to their parameter types, the String callback is not assignable causing the runtime error. In other words, the callback as declared must accept anything, so a function that only accepts strings is not sufficient.

@tvolkert
Copy link
Contributor Author

Well spotted, sir! Closing as working as intended.

@eernstg
Copy link
Member

eernstg commented Jul 20, 2020

Here's another thing which is worth keeping in mind: Even when the type arguments have been added, it is unsafe to have an instance variable like onValue, because it has a declared type which is contravariant in the type parameter T, and the class is covariant in T. For example:

typedef F<X> = void Function(X);

class C<X> {
  F<X> f; // `F` is contravariant in its type parameter: not recommended.
  C(this.f);
}

void main() {
  C<num> c = C<int>((x) {});
  c.f; // Throws.
}

The evaluation of c.f throws (we just need to touch f, we don't even have to call it for the bomb to go off).

The essence of the problem is that c has dynamic type C<int> and static type C<num>. We can make this kind of class safe by adding sound variance (in particular: declaration-site variance, dart-lang/language#524) to the language (in which case we can specify that C<int> is simply not a subtype of C<num>, and hence that kind of dynamic/static type relation never arises).

For now, it's best to avoid contravariant instance variables whenever possible. If it cannot be avoided, it may be possible to maintain the required discipline manually (that is, never use a BugReport<T1> where a BugReport<T2> is expected, unless T1 == T2).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants