Skip to content

Commit

Permalink
Added new feature spec generic-function-instantiation.md.
Browse files Browse the repository at this point in the history
This CL adds a new feature specification for 'generic function
instantiation' (aka partial specialization, type-curried invocation,
etc.), which amounts to implicitly obtaining a non-generic function
from a denotation of a generic function when the typing requires the
latter.

This feature spec relies on type inference, but as a black box (so
it can hardly be incompatible with any current or future version of
type inference in Dart).

Note that it is a restricted version of generic function
instantiation which is specified here, it only supports global and
static functions and instance methods; function literals and first
class function values are not supported.

I just learned from Vijay (Mar 23, 3pm CET) that first class functions
_do_ support generic function instantiation in some existing
implementations. So maybe there is no problem supporting them, and
we should just eliminate that restriction?

Here is a rendered version of the document, refreshed to match patch
set 19: https://gist.github.com/eernstg/bf816d3495e9b87ab6eb958ba707d016

Change-Id: Ie1fd601d3e359bfb5f4616f8ec68a110f42e01b7
Reviewed-on: https://dart-review.googlesource.com/47043
Reviewed-by: Lasse R.H. Nielsen <lrn@google.com>
  • Loading branch information
eernstg committed Jul 11, 2018
1 parent aeb226b commit b8098e9
Showing 1 changed file with 387 additions and 0 deletions.
387 changes: 387 additions & 0 deletions docs/language/informal/generic-function-instantiation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,387 @@
# Generic Function Instantiation

Author: eernst@.

Version: 0.3 (2018-04-05)

Status: Under discussion.

**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&mdash;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.

0 comments on commit b8098e9

Please sign in to comment.