skip to Main Content

I am working on an app using Dart and Flutter. The code below is a simplified and isolated version of the real problem I am running into.

I would like to make a list that holds only items of the generic class X. X has two generic values: Y and Z. I would like the elements in the list to have different values for Y and Z. So [X<Y1, Z1>, X<Y2, Z2>] but I keep getting this exception:

"_TypeError (type ‘(DateFilter, Question) => bool’ is not a subtype of
type ‘(dynamic, dynamic) => bool’)"

I haven’t been able to figure out how to add extra generic type annotations to get it to work.

void main() {
  List<Foo> list = [
    Foo(arg: "hello", func: func1),
    Foo(arg: 100, func: func2),
  ];
}

bool func1(Bar bar) {
  return true;
}

bool func2(Bar bar) {
  return false;
}

class Foo<T, U> {
  final T arg;
  final bool Function(U) func;

  const Foo({
    required this.arg,
    required this.func
  });
}

class Bar {
  final String text;

  const Bar({
    required this.text
  });
}

2

Answers


  1. As mentioned by jamesdlin, List<Foo> is actually List<Foo<dynamic, dynamic>>. Now that you already have a "contract" that the items of list will have the Foo<dynamic, dynamic> type, you also make a "contract" that the function given to its func property will accept a dynamic as its parameter.

    Now here’s an example:

    void main() {
      List<Foo> list = [
        Foo(arg: "hello", func: func1), // Analyzer complains here
        Foo(arg: 100, func: func2), // And here
      ];
    
      final foo1 = list[0];
      dynamic baz = 5;
      final result = foo1.func(baz);
    }
    
    bool func1(Bar bar) {
      // Doing Bar specific actions...
      if (bar.text == 'Hello') return false;
    
      return true;
    }
    

    As you can see, calling foo1.func(baz) should be no problem, because foo1 is a Foo and as we know we made contract the its func property will accept a dynamic as its parameter. But in fact, the func that we passed to the foo1 object is a bool Function(Bar), which may contains Bar specific code that is not applicable to other types. That is why, the Dart analyzer has complained from the moment you passed a bool Function(Bar) to the func parameter which is a bool Function(dynamic).


    There is also a "consumer and producer" concept in substituting types, which defines when a subtype can be replaced by a supertype and when a supertype can be replaced by a subtype.

    See: Substituting types

    Login or Signup to reply.
  2. When you do: List<Foo> list = [Foo(...)], you are declaring list to be of type List<Foo>, which is shorthand for List<Foo<dynamic, dynamic>>. Inference then flows from left-to-right: the elements of the right-hand-side then are inferred to be Foo<dynamic, dynamic> and therefore expect func callbacks that take dynamic arguments.

    If you instead omit the explicit declaration then the elements will be inferred to be Foo<String, Bar> and Foo<int, Bar>, and inference will flow from right-to-left, causing list to be inferred to be List<Foo<Object, Bar>>. (This is why omitting explicit type declarations can be safer. Also see https://stackoverflow.com/a/61807738/.)

    Regarding your comments about wanting to make func1 and func2 take additional T arguments: you could make func1 and func2 generic on T. For example:

    bool func1<T>(Bar bar, T x) {
      return true;
    }
    

    However, while it’s tempting to do only that (and that’s what I initially tried), it doesn’t quite do what’s intended. The inference flow doesn’t allow func1<T> to be inferred from the sibling argument to Foo. (There’s some support for inferring argument types from sibling arguments, but there are limitations. See https://github.com/dart-lang/language/issues/731.)

    Explicitly specifying a type (e.g. Foo('hello', func: func1<String>)) is also problematic if you attempt to invoke the callback functions through list. That is, something like:

    void main() {
      var list = [
        Foo(arg: "hello", func: func1<String>),
        Foo(arg: 100, func: func2<int>),
      ];
      
      var bar = Bar(text: 'hi');
      
      for (var i in list) {
        i.func(bar, i.arg); // ERROR
      }
    }
    

    will fail at runtime:

    Error: TypeError: Closure 'func1' with <String>: type '(Bar, String) => bool' is not a subtype of type '(Bar, Object) => bool'
    

    list is List<Foo<Object, Bar>>, i has a static type of Foo<Object, Bar>, and therefore i.func(bar, i.arg) expects to invoke a Function(Bar, Object) but instead gets a Function(Bar, String), which isn’t substitutable. (Furthermore, i.arg is only statically known to be an Object.)

    If you aren’t invoking the Foo.func methods directly through list (which inherently loses static type information), then it’s not a problem. For example, you could do:

    void main() {
      var list = [
        Foo(arg: "hello", func: func1<String>),
        Foo(arg: 100, func: func2<int>),
      ];
    
      var bar = Bar(text: 'hi');
    
      for (var i in list) {
        i.execute(bar);
      }
    }
    
    bool func1<T>(Bar bar, T x) {
      print('func1<$T>(Bar(${bar.text}), $x)');
      return true;
    }
    
    bool func2<T>(Bar bar, T x) {
      print('func2<$T>(Bar(${bar.text}), $x)');
      return false;
    }
    
    class Foo<T, U> {
      final T arg;
      final bool Function(U, T) func;
    
      bool execute(U u) => func(u, arg);
    
      const Foo({required this.arg, required this.func});
    }
    
    class Bar {
      final String text;
    
      const Bar({required this.text});
    }
    

    which outputs:

    func1<String>(Bar(hi), hello)
    func2<int>(Bar(hi), 100)
    

    showing that func1 and func2 are properly typed.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search