Skip to content

Commit b8098e9

Browse files
committed
Added new feature spec generic-function-instantiation.md.
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 <[email protected]>
1 parent aeb226b commit b8098e9

File tree

1 file changed

+387
-0
lines changed

1 file changed

+387
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
# Generic Function Instantiation
2+
3+
Author: eernst@.
4+
5+
Version: 0.3 (2018-04-05)
6+
7+
Status: Under discussion.
8+
9+
**This document** is a Dart 2 feature specification of _generic function
10+
instantiation_, which is the feature that implicitly coerces a reference to
11+
a generic function into a non-generic function obtained from said generic
12+
function by passing inferred type arguments.
13+
14+
Intuitively, this is the feature that provides inference for function
15+
values, corresponding to the more well-known inference that we may get for
16+
each invocation of a generic function:
17+
18+
```dart
19+
List<T> f<T>(T t) => [t];
20+
21+
void g(Iterable<int> f(int i)) => print(f(42));
22+
23+
main() {
24+
// Invocation inference.
25+
print(f(42)); // Inferred as `f<int>(42)`.
26+
27+
// Function value inference.
28+
g(f); // Inferred approximately as `g((int n) => f<int>(n))`.
29+
}
30+
```
31+
32+
This document draws on many of the comments on the SDK issue
33+
[#31665](https://github.com/dart-lang/sdk/issues/31665).
34+
35+
36+
## Motivation
37+
38+
The
39+
[language specification](https://github.com/dart-lang/sdk/blob/master/docs/language/dartLangSpec.tex)
40+
uses the phrase _function object_ to denote the first-class semantic
41+
entity which corresponds to a function declaration. In the following
42+
example, each of the expressions `fg`, `A.fs`, `new A().fi`, and `fl` in
43+
`main` evaluate to a function object, and so does the function literal at
44+
the end of the list:
45+
46+
```dart
47+
int fg(int i) => i;
48+
49+
class A {
50+
static int fs(int i) => i;
51+
int fi(int i) => i;
52+
}
53+
54+
main() {
55+
int fl(int i) => i;
56+
var functions =
57+
[fg, A.fs, new A().fi, fl, (int i) => i];
58+
}
59+
```
60+
61+
Once a function object has been obtained, it can be passed around by
62+
assigning it to a variable, passing it as an actual argument, etc. Hence,
63+
it is the notion of a function object that makes functions first-class
64+
entities. The computational step that produces a function object from a
65+
denotation of a function declaration is known as _closurization_.
66+
67+
The situation where closurization occurs is exactly the situation where the
68+
generic function instantiation feature specified in this document may kick
69+
in.
70+
71+
First note that generic function declarations provide support for working
72+
with generic functions as first class values, i.e., generic functions
73+
support regular closurization, just like non-generic functions.
74+
75+
The essence of generic function instantiation is to allow for "curried"
76+
invocations, in the sense that a generic function can receive its actual
77+
type arguments separately during closurization (it must then receive _all_
78+
its type arguments, not just some of them), and that yields a non-generic
79+
function whose type is obtained by substituting type variables in the
80+
generic type for the actual type arguments:
81+
82+
```dart
83+
X fg<X extends num>(X x) => x;
84+
85+
class A {
86+
static X fs<X extends num>(X x) => x;
87+
X fi<X extends num>(X x) => x;
88+
}
89+
90+
main() {
91+
X fl<X extends num>(X x) => x;
92+
var genericFunctions =
93+
<Function>[fg, A.fs, new A().fi, fl, <X>(X x) => x];
94+
var instantiatedFunctions =
95+
<int Function(int)>[fg, A.fs, new A().fi, fl];
96+
}
97+
```
98+
99+
The functions stored in `instantiatedFunctions` are all of type
100+
`int Function(int)`, and they are obtained by passing the actual
101+
type argument `int` to the denoted generic function, thus obtaining
102+
a non-generic function of the specified type. Hence, the reason why
103+
`instantiatedFunctions` can be created as shown is that it relies on
104+
generic function instantiation, for each element in the list.
105+
106+
Note that generic function instantiation is not supported with all kinds of
107+
generic functions; this is discussed in the discussion section.
108+
109+
It may seem natural to allow explicit instantiations, e.g., `fg<int>` and
110+
`new A().fi<int>` (where type arguments are passed explicitly, but there is
111+
no value argument list). This kind of construct would yield non-generic
112+
functions, just like the cases shown above where the type arguments are
113+
inferred. This is a language extension which is not included in this
114+
document. It may or may not be added to the language separately.
115+
116+
117+
## Syntax
118+
119+
This feature does not affect the grammar.
120+
121+
*If this feature is generalized to include explicit generic
122+
function instantiation, the grammar would need to be extended
123+
to allow a construct like `f<int>` as an expression.*
124+
125+
126+
## Static Analysis and Program Transformation
127+
128+
We say that a reference of the form `identifier`,
129+
`identifier '.' identifier`, or
130+
`identifier '.' identifier '.' identifier`
131+
is a _statically resolved reference to a function_ if it denotes a
132+
declaration of a library function or a static function.
133+
134+
*Such a reference is first-order in the sense that it is bound directly to
135+
the function declaration and there need not be a heap object which
136+
represents said function declaration in order to support invocations of the
137+
function. In that sense we may consider statically resolved references
138+
"extra simple", compared to general references to functions. In particular,
139+
a statically resolved reference to a function will have a static type which
140+
is obtained directly from its declaration, it will never be a supertype
141+
thereof such as `Function` or `dynamic`.*
142+
143+
When an expression _e_ whose static type is a generic function type _G_ is
144+
used in a context where the expected type is a non-generic function type
145+
_F_, it is a compile-time error except in the three situations specified
146+
below.
147+
148+
*The point is that generic function instantiation will only take place in
149+
situations where we would have a compile-time error without that feature,
150+
and in those situations the compile-time error will still exist unless the
151+
situation matches one of those three exceptions.*
152+
153+
**1st exception**: If _e_ is a statically resolved reference to a function,
154+
and type inference yields an actual type argument list
155+
_T<sub>1</sub> .. T<sub>k</sub>_ such that
156+
_G<T<sub>1</sub> .. T<sub>k</sub>>_ is assignable to _F_, then the program
157+
is modified such that _e_ is replaced by a reference to a non-generic
158+
function whose signature is obtained by substituting
159+
_T<sub>1</sub> .. T<sub>k</sub>_ for the formal type parameters in the
160+
function signature of the function denoted by _e_, and whose semantics for
161+
each invocation is the same as invoking _e_ with
162+
_T<sub>1</sub> .. T<sub>k</sub>_ as the actual type argument list.
163+
164+
*Here is an example:*
165+
166+
```dart
167+
List<T> foo<T>(T t) => [t];
168+
List<int> fooOfInt(int i) => [i];
169+
170+
String bar(List<int> f(int)) => "${f(42)}";
171+
172+
main() {
173+
print(bar(foo));
174+
}
175+
```
176+
177+
*In this example, `foo` as an actual argument to `bar` will be modified as
178+
if the call had been `bar(fooOfInt)`, except for equality&mdash;which is
179+
specified next.*
180+
181+
Consider two distinct evaluations of a statically resolved reference to the
182+
same generic function, which are subject to the above-mentioned
183+
transformation with the same actual type argument list, and let `f1` and
184+
`f2` denote the two functions obtained after the transformation. It is then
185+
guaranteed that `f1 == f2` evaluates to true, but `identical(f1, f2)` can
186+
be false or true, depending on the implementation.
187+
188+
**2nd exception**: Generic function instantiation is supported for instance
189+
methods as well as statically resolved functions: If
190+
191+
- _e_ is a property extraction which denotes a closurization,
192+
- the static type of _e_ is a generic function type _G_,
193+
- _e_ occurs in a context where the expected type is a non-generic
194+
function type _F_, and
195+
- type inference yields an actual type argument list
196+
_T<sub>1</sub> .. T<sub>k</sub>_ such that
197+
_G<T<sub>1</sub> .. T<sub>k</sub>>_ is assignable to _F_
198+
199+
then the program is modified such that _e_ is replaced by a reference to a
200+
non-generic function whose signature is obtained by substituting
201+
_T<sub>1</sub> .. T<sub>k</sub>_ for
202+
the formal type parameters in the signature of the method denoted by
203+
_e_, and whose semantics for each invocation is the same as
204+
invoking that method on that receiver with
205+
_T<sub>1</sub> .. T<sub>k</sub>_ as the actual type argument list.
206+
207+
*Note that the statically known declaration of the method which is
208+
closurized may not be the same one as the declaration of the method which
209+
is actually closurized at run time, but it is guaranteed that the actual
210+
signature will have a formal type parameter list with the same length,
211+
where each formal type parameter will have the same bound as the statically
212+
known one, and the value parameters will have types which are in a correct
213+
override relationship to the statically known ones. In other words, the
214+
function obtained by generic function instantiation on an instance method
215+
may accept a different number of parameters, with type annotations that are
216+
different than the statically known ones, but the corresponding function
217+
type will be a subtype of the statically known one, i.e., it can be called
218+
safely. (It is possible that the method which is actually closurized has
219+
one or more formal parameters which are covariant, and this may cause an
220+
otherwise statically safe invocation to fail at run-time, but this is
221+
exactly the same situation as we would have had with a direct invocation of
222+
the method.)*
223+
224+
Consider two distinct evaluations of a property extraction for the same method
225+
of receivers `o1` and `o2`, which are subject to the above-mentioned
226+
transformation with the same actual type argument list, and let `f1` and
227+
`f2` denote the two functions obtained after the transformation. It is then
228+
guaranteed that `f1 == f2` evaluates to the same value as `identical(o1, o2)`,
229+
but `identical(f1, f2)` can be false or true, depending on the implementation.
230+
231+
*Here is an example:*
232+
233+
```dart
234+
class A {
235+
List<T> foo<T>(T t) => [t];
236+
}
237+
238+
String bar(List<int> f(int)) => "${f(42)}";
239+
240+
main() {
241+
print(bar(new A().foo));
242+
}
243+
```
244+
245+
*In this example, `new A().foo` as an actual argument to `bar` will be
246+
modified as if the call had been `bar((int i) => o.foo<int>(i))` where `o`
247+
is a fresh variable bound to the result of evaluating `new A()`, except for
248+
equality.*
249+
250+
**3rd exception**: Generic function instantiation is supported also for
251+
local functions: If
252+
253+
- _e_ is an `identifier` denoting a local function,
254+
- the static type of _e_ is a generic function type _G_,
255+
- _e_ occurs in a context where the expected type is a non-generic
256+
function type _F_, and
257+
- type inference yields an actual type argument list
258+
_T<sub>1</sub> .. T<sub>k</sub>_ such that
259+
_G<T<sub>1</sub> .. T<sub>k</sub>>_ is assignable to _F_
260+
261+
then the program is modified such that _e_ is replaced by a reference to a
262+
non-generic function whose signature is obtained by substituting
263+
_T<sub>1</sub> .. T<sub>k</sub>_ for
264+
the formal type parameters in the signature of the function denoted by _e_,
265+
and whose semantics for each invocation is the same as invoking that
266+
function on that receiver with _T<sub>1</sub> .. T<sub>k</sub>_ as the
267+
actual type argument list.
268+
269+
*No special guarantees are provided regarding the equality and identity
270+
properties of the non-generic functions obtained from a local function.*
271+
272+
If _e_ is an expression which is subject to generic function instantiation
273+
as specified above, and the function denoted by _e_ is a top-level function
274+
or a static method that is not qualified by a deferred prefix, and the
275+
inferred type arguments are all compile-time constant type expressions
276+
(*cf. [this CL](https://dart-review.googlesource.com/c/sdk/+/36220)*), then
277+
_e_ is a constant expression. Other than that, an expression subject to
278+
generic function instantiation is not constant.
279+
280+
281+
## Dynamic Semantics
282+
283+
The dynamic semantics of this feature follows directly from the fact that
284+
the section on static analysis specifies which expressions are subject to
285+
generic function instantiation, and how to obtain the non-generic function
286+
which is the value of such an expression.
287+
288+
There is one exception: It is possible for inference to provide a type
289+
argument which is not statically guaranteed to satisfy the declared upper
290+
bound. In that case, a dynamic error occurs when the generic function
291+
instantiation takes place.
292+
293+
*Here is an example to illustrate how this may occur:*
294+
295+
```dart
296+
class C<X> {
297+
X x;
298+
void foo<Y extends X>(Y y) => x = y;
299+
}
300+
301+
C<num> complexComputation() => new C<int>();
302+
303+
main() {
304+
C<num> c = complexComputation();
305+
void Function(num) f = c.foo; // Inferred type argument: `num`.
306+
}
307+
```
308+
309+
*In this situation, the inferred type argument `num` is not guaranteed to
310+
satisfy the declared upper bound of `Y`, because the actual type argument
311+
of `c`, let us call it `T`, is only known to be some subtype of `num`.
312+
There is no way to denote the type `T` or any other type (except `Null`)
313+
which is guaranteed to be a subtype of `T`. Hence, the chosen type argument
314+
may turn out to violate the bound at run time, and that violation must be
315+
detected when the tear-off takes place, rather than letting the tear-off
316+
succeed and incurring a dynamic error at each invocation of the resulting
317+
function object.*
318+
319+
320+
## Discussion
321+
322+
There is no support for generic function instantiation with function
323+
literals. That is hardly a serious omission, however, because a function
324+
literal is only referred from one single location (the place where it
325+
occurs), and hence there is never a need to use such a function both as a
326+
generic and as a non-generic function, so it is extremely likely to be
327+
simpler and more convenient to write the function literal as a non-generic
328+
function in the first place, if that is how it will be used.
329+
330+
```dart
331+
class A<X> {
332+
X x;
333+
334+
A(this.x);
335+
336+
void f(List<X> Function(X) g) => print(g(x));
337+
338+
void bar() {
339+
// Error: Needs generic function instantiation,
340+
// which would implicitly pass `<X>`.
341+
f(<Y>(Y y) => [y]);
342+
343+
// Work-around: Just use a non-generic function---it can get
344+
// the required different types for different values of `X` by
345+
// using `X` directly.
346+
f((X x) => [x]);
347+
}
348+
}
349+
350+
main() {
351+
new A<int>(42).bar();
352+
}
353+
```
354+
355+
Finally, there is no support for generic function instantiation with first
356+
class functions (e.g., the value of a variable or an actual argument). This
357+
choice was made in order to avoid the complexity and performance
358+
implications of having such a feature. Note that, apart from the `==`
359+
property, it is always possible to write a function literal in order to
360+
pass actual type arguments explicitly, thus getting the same effect:
361+
362+
```dart
363+
List<T> foo<T>(T t) => [t];
364+
365+
void g(List<int> Function(int) h) => print(h(42)[0].isEven);
366+
367+
void bar(List<T> Function<T>(T) f) {
368+
g(f); // Error: Generic function instantiation not supported here.
369+
// Work-around.
370+
g((int i) => f(i));
371+
}
372+
373+
main() {
374+
bar(foo); // No generic function instantiation needed here.
375+
}
376+
```
377+
378+
379+
## Revisions
380+
381+
- 0.3 (2018-04-05) Clarified constancy of expressions subject to generic
382+
function instantiation.
383+
384+
- 0.2 (2018-03-21) Adjusted to include support for generic function
385+
instantiation also for local functions.
386+
387+
- 0.1 (2018-03-19) Initial version.

0 commit comments

Comments
 (0)