Permalink
Find file
c1da470 Dec 6, 2016
@munificent @vsmenon
189 lines (143 sloc) 5.17 KB

Using Generic Methods

Note: For historical reasons, this feature is called "generic methods", but it applies equally well to instance methods, static methods, top-level functions, local functions, and even lambda expressions.

Initially a proposal, generic methods are on their way to being fully supported in Dart. Here is how to use them.

When they were still being prototyped, an older comment-based syntax was designed so that the static analysis could be implemented and tested before the VM and compilers needed to worry about the syntax. Now that real syntax is allowed everywhere, this doc has been updated.

Declaring generic methods

Type parameters for generic methods are listed after the method or function name, inside angle brackets:

/// Takes two type parameters, [K] and [V].
Map<K, V> singletonMap<K, V>(K key, V value) {
  return <K, V>{ key, value };
}

As with classes, you can put bounds on type parameters:

/// Takes a list of two numbers of some num-derived type [T].
T sumPair<T extends num>(List<T> items) {
  return items[0] + items[1];
}

Class methods (instance and static) can be declared to take generic parameters in the same way:

class C {
  static int f<S, T>(int x) => 3;
  int m<S, T>(int x) => 3;
}

This even works for function-typed parameters, local functions, and function expressions:

/// Takes a generic method as a parameter [callback].
void functionTypedParameter(T callback<T>(T thing)) {}

// Declares a local generic function `itself`.
void localFunction() {
  T itself<T>(T thing) => thing;
}

// Binds a generic function expression to a local variable.
void functionExpression() {
  var lambda = <T>(T thing) => thing;
}

We do not currently support a way to declare a function as returning a generic function. This will eventually be supported using a typedef.

Using generic method type parameters

You've seen some examples already, but you can use a generic type parameter almost anywhere you would expect in a generic method.

  • Inside the method's parameter list:

    takeThing<T>(T thing) { ... }
    //           ^-- Here.
  • Inside type annotations in the body of the method:

    useThing<T>() {
      T thing = getThing();
    //^-- Here.
      List<T> pair = [thing, thing];
      //   ^-- And here.
    }
  • In the return type of the method:

      T itself<T>(T thing) => thing;
    //^-- Here.
  • As type arguments in generic classes and method calls:

    useThing<T>(T thing) {
      var pair = <T>[thing, thing];
      //          ^-- Here.
      var set = new Set<T>()..add(thing);
      //                ^-- And here.
    }

    Note that generic methods are not yet supported at runtime on the VM and dartjs. On those platforms, uses of generic method type arguments are treated like dynamic today. So in this example, pair's reified type at runtime will be List<dynamic> and set will be Set<dynamic>.

There are two places you cannot use a generic method type parameter. Both are because the VM and dart2js don't support reifying generic method type arguments yet. Since these expressions wouldn't do what you want, we've temporarily defined them to be an error:

  • As the right-hand side of an is or is! expression.

    testType<T>(object) {
      print(object is T);
      //              ^-- Error!
      print(object is! T);
      //               ^-- Error!
    }
  • As a type literal:

    printType<T>() {
      Type t = T;
      //       ^-- Error!
      print(t);
    }

Once we have full runtime support for generic methods, these will be allowed.

Calling generic methods

Most of the time, when you call a generic method, you can leave off the type arguments and strong mode's type inference will fill them in for you automatically. For example:

var fruits = ["apple", "banana", "cherry"];
var lengths = fruits.map((fruit) => fruit.length);

The map() method on Iterable is now generic and takes a type parameter for the element type of the returned sequence:

class Iterable<T> {
  Iterable<S> map<S>(S transform(T element)) { ... }

  // Other stuff...
}

In this example, the type checker:

  1. Infers List<String> for the type of fruits based on the elements in the list literal.
  2. That lets it infer String for the type of the lambda parameter fruit passed to map().
  3. Then, from the result of calling .length, it infers the return type of the lambda to be int.
  4. That in turn is used to fill in the type argument to the call to map() as int, and the resulting sequence is an Iterable<int>.

If inference isn't able to fill in a type argument for you, it uses dynamic instead. If that isn't what you want, or it infers a type you don't want, you can always pass them explicitly:

// Explicitly give a type so that we don't infer "int".
var lengths = fruits.map<num>((fruit) => fruit.length).toList();

// So that we can later add doubles to the result.
lengths.add(1.2);