Change-Id: I3afa22ce86bb7e3e9a24bc995aca8610e70193de Reviewed-on: https://dart-review.googlesource.com/c/87184 Reviewed-by: Lasse R.H. Nielsen <lrn@google.com>
14 KiB
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.