Generic Function Instantiation

Author: eernst@.

Version: 0.3 (2018-04-05)

Status: This document is now background material. For normative text, please consult the language specification.

This document is a Dart 2 feature specification of generic function instantiation, which is the feature that implicitly coerces a reference to a generic function into a non-generic function obtained from said generic function by passing inferred type arguments.

Intuitively, this is the feature that provides inference for function values, corresponding to the more well-known inference that we may get for each invocation of a generic function:

List<T> f<T>(T t) => [t];

void g(Iterable<int> f(int i)) => print(f(42));

main() {
  // Invocation inference.
  print(f(42)); // Inferred as `f<int>(42)`.

  // Function value inference.
  g(f); // Inferred approximately as `g((int n) => f<int>(n))`.
}

This document draws on many of the comments on the SDK issue #31665.

Motivation

The language specification uses the phrase function object to denote the first-class semantic entity which corresponds to a function declaration. In the following example, each of the expressions fg, A.fs, new A().fi, and fl in main evaluate to a function object, and so does the function literal at the end of the list:

int fg(int i) => i;

class A {
  static int fs(int i) => i;
  int fi(int i) => i;
}

main() {
  int fl(int i) => i;
  var functions =
      [fg, A.fs, new A().fi, fl, (int i) => i];
}

Once a function object has been obtained, it can be passed around by assigning it to a variable, passing it as an actual argument, etc. Hence, it is the notion of a function object that makes functions first-class entities. The computational step that produces a function object from a denotation of a function declaration is known as closurization.

The situation where closurization occurs is exactly the situation where the generic function instantiation feature specified in this document may kick in.

First note that generic function declarations provide support for working with generic functions as first class values, i.e., generic functions support regular closurization, just like non-generic functions.

The essence of generic function instantiation is to allow for “curried” invocations, in the sense that a generic function can receive its actual type arguments separately during closurization (it must then receive all its type arguments, not just some of them), and that yields a non-generic function whose type is obtained by substituting type variables in the generic type for the actual type arguments:

X fg<X extends num>(X x) => x;

class A {
  static X fs<X extends num>(X x) => x;
  X fi<X extends num>(X x) => x;
}

main() {
  X fl<X extends num>(X x) => x;
  var genericFunctions =
      <Function>[fg, A.fs, new A().fi, fl, <X>(X x) => x];
  var instantiatedFunctions =
      <int Function(int)>[fg, A.fs, new A().fi, fl];
}

The functions stored in instantiatedFunctions are all of type int Function(int), and they are obtained by passing the actual type argument int to the denoted generic function, thus obtaining a non-generic function of the specified type. Hence, the reason why instantiatedFunctions can be created as shown is that it relies on generic function instantiation, for each element in the list.

Note that generic function instantiation is not supported with all kinds of generic functions; this is discussed in the discussion section.

It may seem natural to allow explicit instantiations, e.g., fg<int> and new A().fi<int> (where type arguments are passed explicitly, but there is no value argument list). This kind of construct would yield non-generic functions, just like the cases shown above where the type arguments are inferred. This is a language extension which is not included in this document. It may or may not be added to the language separately.

Syntax

This feature does not affect the grammar.

If this feature is generalized to include explicit generic function instantiation, the grammar would need to be extended to allow a construct like f<int> as an expression.

Static Analysis and Program Transformation

We say that a reference of the form identifier, identifier '.' identifier, or identifier '.' identifier '.' identifier is a statically resolved reference to a function if it denotes a declaration of a library function or a static function.

Such a reference is first-order in the sense that it is bound directly to the function declaration and there need not be a heap object which represents said function declaration in order to support invocations of the function. In that sense we may consider statically resolved references “extra simple”, compared to general references to functions. In particular, a statically resolved reference to a function will have a static type which is obtained directly from its declaration, it will never be a supertype thereof such as Function or dynamic.

When an expression e whose static type is a generic function type G is used in a context where the expected type is a non-generic function type F, it is a compile-time error except in the three situations specified below.

The point is that generic function instantiation will only take place in situations where we would have a compile-time error without that feature, and in those situations the compile-time error will still exist unless the situation matches one of those three exceptions.

1st exception: If e is a statically resolved reference to a function, and type inference yields an actual type argument list T1 .. Tk such that G<T1 .. Tk> is assignable to F, then the program is modified such that e is replaced by a reference to a non-generic function whose signature is obtained by substituting T1 .. Tk for the formal type parameters in the function signature of the function denoted by e, and whose semantics for each invocation is the same as invoking e with T1 .. Tk as the actual type argument list.

Here is an example:

List<T> foo<T>(T t) => [t];
List<int> fooOfInt(int i) => [i];

String bar(List<int> f(int)) => "${f(42)}";

main() {
  print(bar(foo));
}

In this example, foo as an actual argument to bar will be modified as if the call had been bar(fooOfInt), except for equality—which is specified next.

Consider two distinct evaluations of a statically resolved reference to the same generic function, which are subject to the above-mentioned transformation with the same actual type argument list, and let f1 and f2 denote the two functions obtained after the transformation. It is then guaranteed that f1 == f2 evaluates to true, but identical(f1, f2) can be false or true, depending on the implementation.

2nd exception: Generic function instantiation is supported for instance methods as well as statically resolved functions: If

  • e is a property extraction which denotes a closurization,
  • the static type of e is a generic function type G,
  • e occurs in a context where the expected type is a non-generic function type F, and
  • type inference yields an actual type argument list T1 .. Tk such that G<T1 .. Tk> is assignable to F

then the program is modified such that e is replaced by a reference to a non-generic function whose signature is obtained by substituting T1 .. Tk for the formal type parameters in the signature of the method denoted by e, and whose semantics for each invocation is the same as invoking that method on that receiver with T1 .. Tk as the actual type argument list.

Note that the statically known declaration of the method which is closurized may not be the same one as the declaration of the method which is actually closurized at run time, but it is guaranteed that the actual signature will have a formal type parameter list with the same length, where each formal type parameter will have the same bound as the statically known one, and the value parameters will have types which are in a correct override relationship to the statically known ones. In other words, the function obtained by generic function instantiation on an instance method may accept a different number of parameters, with type annotations that are different than the statically known ones, but the corresponding function type will be a subtype of the statically known one, i.e., it can be called safely. (It is possible that the method which is actually closurized has one or more formal parameters which are covariant, and this may cause an otherwise statically safe invocation to fail at run-time, but this is exactly the same situation as we would have had with a direct invocation of the method.)

Consider two distinct evaluations of a property extraction for the same method of receivers o1 and o2, which are subject to the above-mentioned transformation with the same actual type argument list, and let f1 and f2 denote the two functions obtained after the transformation. It is then guaranteed that f1 == f2 evaluates to the same value as identical(o1, o2), but identical(f1, f2) can be false or true, depending on the implementation.

Here is an example:

class A {
  List<T> foo<T>(T t) => [t];
}

String bar(List<int> f(int)) => "${f(42)}";

main() {
  print(bar(new A().foo));
}

In this example, new A().foo as an actual argument to bar will be modified as if the call had been bar((int i) => o.foo<int>(i)) where o is a fresh variable bound to the result of evaluating new A(), except for equality.

3rd exception: Generic function instantiation is supported also for local functions: If

  • e is an identifier denoting a local function,
  • the static type of e is a generic function type G,
  • e occurs in a context where the expected type is a non-generic function type F, and
  • type inference yields an actual type argument list T1 .. Tk such that G<T1 .. Tk> is assignable to F

then the program is modified such that e is replaced by a reference to a non-generic function whose signature is obtained by substituting T1 .. Tk for the formal type parameters in the signature of the function denoted by e, and whose semantics for each invocation is the same as invoking that function on that receiver with T1 .. Tk as the actual type argument list.

No special guarantees are provided regarding the equality and identity properties of the non-generic functions obtained from a local function.

If e is an expression which is subject to generic function instantiation as specified above, and the function denoted by e is a top-level function or a static method that is not qualified by a deferred prefix, and the inferred type arguments are all compile-time constant type expressions (cf. this CL), then e is a constant expression. Other than that, an expression subject to generic function instantiation is not constant.

Dynamic Semantics

The dynamic semantics of this feature follows directly from the fact that the section on static analysis specifies which expressions are subject to generic function instantiation, and how to obtain the non-generic function which is the value of such an expression.

There is one exception: It is possible for inference to provide a type argument which is not statically guaranteed to satisfy the declared upper bound. In that case, a dynamic error occurs when the generic function instantiation takes place.

Here is an example to illustrate how this may occur:

class C<X> {
  X x;
  void foo<Y extends X>(Y y) => x = y;
}

C<num> complexComputation() => new C<int>();

main() {
  C<num> c = complexComputation();
  void Function(num) f = c.foo; // Inferred type argument: `num`.
}

In this situation, the inferred type argument num is not guaranteed to satisfy the declared upper bound of Y, because the actual type argument of c, let us call it T, is only known to be some subtype of num. There is no way to denote the type T or any other type (except Null) which is guaranteed to be a subtype of T. Hence, the chosen type argument may turn out to violate the bound at run time, and that violation must be detected when the tear-off takes place, rather than letting the tear-off succeed and incurring a dynamic error at each invocation of the resulting function object.

Discussion

There is no support for generic function instantiation with function literals. That is hardly a serious omission, however, because a function literal is only referred from one single location (the place where it occurs), and hence there is never a need to use such a function both as a generic and as a non-generic function, so it is extremely likely to be simpler and more convenient to write the function literal as a non-generic function in the first place, if that is how it will be used.

class A<X> {
  X x;

  A(this.x);

  void f(List<X> Function(X) g) => print(g(x));

  void bar() {
    // Error: Needs generic function instantiation,
    // which would implicitly pass `<X>`.
    f(<Y>(Y y) => [y]);

    // Work-around: Just use a non-generic function---it can get
    // the required different types for different values of `X` by
    // using `X` directly.
    f((X x) => [x]);
  }
}

main() {
  new A<int>(42).bar();
}

Finally, there is no support for generic function instantiation with first class functions (e.g., the value of a variable or an actual argument). This choice was made in order to avoid the complexity and performance implications of having such a feature. Note that, apart from the == property, it is always possible to write a function literal in order to pass actual type arguments explicitly, thus getting the same effect:

List<T> foo<T>(T t) => [t];

void g(List<int> Function(int) h) => print(h(42)[0].isEven);

void bar(List<T> Function<T>(T) f) {
  g(f); // Error: Generic function instantiation not supported here.
  // Work-around.
  g((int i) => f(i));
}

main() {
  bar(foo); // No generic function instantiation needed here.
}

Revisions

  • 0.3 (2018-04-05) Clarified constancy of expressions subject to generic function instantiation.

  • 0.2 (2018-03-21) Adjusted to include support for generic function instantiation also for local functions.

  • 0.1 (2018-03-19) Initial version.