d51a847891
Change-Id: I3afa22ce86bb7e3e9a24bc995aca8610e70193de Reviewed-on: https://dart-review.googlesource.com/c/87184 Reviewed-by: Lasse R.H. Nielsen <lrn@google.com>
389 lines
14 KiB
Markdown
389 lines
14 KiB
Markdown
# 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:
|
|
|
|
```dart
|
|
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](https://github.com/dart-lang/sdk/issues/31665).
|
|
|
|
|
|
## Motivation
|
|
|
|
The
|
|
[language specification](https://github.com/dart-lang/sdk/blob/master/docs/language/dartLangSpec.tex)
|
|
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:
|
|
|
|
```dart
|
|
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:
|
|
|
|
```dart
|
|
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
|
|
_T<sub>1</sub> .. T<sub>k</sub>_ such that
|
|
_G<T<sub>1</sub> .. T<sub>k</sub>>_ 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
|
|
_T<sub>1</sub> .. T<sub>k</sub>_ 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
|
|
_T<sub>1</sub> .. T<sub>k</sub>_ as the actual type argument list.
|
|
|
|
*Here is an example:*
|
|
|
|
```dart
|
|
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
|
|
_T<sub>1</sub> .. T<sub>k</sub>_ such that
|
|
_G<T<sub>1</sub> .. T<sub>k</sub>>_ 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
|
|
_T<sub>1</sub> .. T<sub>k</sub>_ 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
|
|
_T<sub>1</sub> .. T<sub>k</sub>_ 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:*
|
|
|
|
```dart
|
|
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
|
|
_T<sub>1</sub> .. T<sub>k</sub>_ such that
|
|
_G<T<sub>1</sub> .. T<sub>k</sub>>_ 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
|
|
_T<sub>1</sub> .. T<sub>k</sub>_ 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 _T<sub>1</sub> .. T<sub>k</sub>_ 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](https://dart-review.googlesource.com/c/sdk/+/36220)*), 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:*
|
|
|
|
```dart
|
|
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.
|
|
|
|
```dart
|
|
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:
|
|
|
|
```dart
|
|
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.
|