dart-lang / dart-lang/language
Abbreviated function literals
- Dominant language
- TeX
- Stars
- 2.9k
- Forks
- 239
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 14
Description
This is a proposal to add support for _abbreviated function literals_ to Dart.
It is based on discussions about [anonymous methods](https://github.com/dart-lang/language/issues/260), and may be used in combination with a pipe operator to achieve expressions of a similar form as an anonymous method. A similar mechanism exists [in Kotlin](https://kotlinlang.org/docs/reference/lambdas.html#it-implicit-name-of-a-single-parameter).
[Edit April 24, 2019: Spelled out the design choice of naming the implicit parameter `this`. May 13th: Removed parts about the name `this` and introduced use of context type; see [this comment](https://github.com/dart-lang/language/issues/265#issuecomment-491984357) for more details.]
## Overview
Function literals are used frequently in Dart, and many of them declare exactly zero or one required parameter, and no optional parameters. The declaration of such a parameter can be omitted if a standard name is chosen for it, and if a suitable parameter type can be obtained from type inference.
Syntactically, a block function literal with no parameters already has the form `() { ... }`, so we cannot just omit the parameter declaration in order to obtain an abbreviated declaration. A plain `` could be used (that is `{ ... }` containing statements). This creates some parsing ambiguities, but they are not hard to resolve (as specified below).
It is already a compile-time error to have an expression statement starting with `{`, so there is no ambiguity for a block in a sequence of statements: That is just a regular block, that is, some more statements in a nested static scope.
For function literals of the form `(someName) => e`, we can use the abbreviation `=> e`, provided that `someName` is the name which is used for a parameter which is declared implicitly. In this case there are no syntactic conflicts. Similarly, we can abbreviate `() => e` to `=> e`. With both abbreviations we need to disambiguate the form with zero parameters and the one with one parameter, but we can do this in a way which is already used in many situations in Dart: Based on the context type.
Following Kotlin and using `it` as the name of implicitly declared parameters, here are some examples:
```dart
main() {
var xs = [1, 2, 3];
// Current form.
xs.forEach((x) { print(x); });
// Proposed abbreviation.
xs.forEach({ print(it); });
xs.forEach(=> print(it));
// Current form.
var callbacks1 = [() { doA(); }, () => doB()];
// Proposed abbreviation.
var callbacks2 = [{ doA(); }, => doB()];
}
```
Asynchronous and generator variants can be expressed by adding the relevant keyword in front, e.g., `[2, 3].map(sync* { yield it; yield it; })`, which will evaluate to an `Iterable>` that would print as `((2, 2), (3, 3))`.
## Syntax
The grammar is adjusted as follows in order to support abbreviated function literals:
```ebnf
::=
?
::=
?
::=
| ('sync' '*' | 'async' '*')?
::=
'{' '}'
```
*We insist that a block which is used to specify a function literal cannot be empty. This resolves the ambiguity with set and map literals.*
## Static Analysis and Dynamic Semantics
Let `e` be an expression of the form `` that occurs such that it has a context type of the form `T Function(S)` for some types `T` and `S`; `e` is then treated as `(it) e`. Similarly, a term `B` of the form `` or `` with such a context type is treated as `(it) B`.
Let `e` be an expression of the form `` that occurs such that does _not_ have a context type of the form `T Function(S)` for any type `T` and `S`; `e` is then treated as `() e`. Similarly, a term `B` of the form `` or `` with such a context type is treated as `() B`.
*This determines the static analysis and type inference, as well as the dynamic semantics.*
## Discussion
This is a tiny piece of syntactic sugar, but it might be justified by (1) the expected widespread usage, and (2) the standardization effect (which allows developers to read a function at a glance because the chosen parameter name is immediately recognized).
An argument against having this abbreviation is that it creates yet another meaning for an already very frequently seen construct, the `{ ... }` block. The usage of `=> e` might be less confusing in this respect, because the token `=>` already serves to indicate that "this is a function literal".
The proposal makes `{...}` mean `() {...}` in the case when there is no context type or only a loose one like `dynamic`, and it only means `(it) {...}` when the context type is a function type with one positional parameter. We could easily have chosen the opposite, but the given choice is motivated by the typing properties:
If we make the opposite choice (such that `{...}` "by default" means `(it) {...}`), and the context type is `Function` or any top type (e.g., `dynamic`) then the parameter `it` will get the type `dynamic`, and this is likely to introduce a large amount of dynamic typing in the body, silently.
When the context type doesn't match any of the cases mentioned above we will have a compile-time error. In that case the error message should explicitly say something like "the value expected here cannot be an abbreviated function literal". We may be able to say that the desugaring is undefined in this case, but it seems more practical to decide that `{...}` is desugared to a specific term with a specific type, such that we can emit the normal "isn't assignable to" error message as well.
We _could_ support some other argument list shapes. For example, `{...}` could mean `({int a, double b}) {...}` when the context type is `T Function({int a, double b})`, and we could in general handle named parameters, plus zero or one positional parameter (named `it`). However, it probably wouldn't be very easy to _understand_ such a function body, because the declaration of the named parameters are not shown anywhere locally. Hence, no such mechanisms are included in this proposal.
Another thing to keep in mind is that it might be somewhat tricky to see exactly where any given occurrence of the identifier which is the name of the implicitly declared formal parameter is declared:
```dart
import 'lib.dart'; // Assume that `lib.dart` declares something named `it`.
main() {
print({ it }); // Set literal containing `it` declared in 'lib.dart'.
print({ it; }); // Function literal containing a no-op evaluation of its argument.
}
```
We believe that this will not be a serious problem in practice, because the widespread use of the implicit parameter name `it` will make it obvious that it is a really bad idea to declare the same name globally.
It may be considered confusing to have modifiers like `async` as specified. This is a completely optional part of the proposal, and we could just take it out. Developers would then have to write an explicit parameter part in the case where they want to use an asynchronous function or a generator function.
Revisiting the request in #259 and the examples (version 1, 2, and 3) in there, we could express a solution similar to version 4, #260, using abbreviated function literals and the pipe operator (#43) as follows:
```dart
// Variant of version 4, #260, using abbreviated function literals and the pipe operator.
void beginFrame(Duration timeStamp) {
// ...
ui.ParagraphBuilder(
ui.ParagraphStyle(textDirection: ui.TextDirection.ltr),
) -> {
it.addText('Hello, world.');
it.build() -> {
it.layout(ui.ParagraphConstraints(width: logicalSize.width));
canvas.drawParagraph(it, ui.Offset(...));
};
};
ui.SceneBuilder() -> {
it.pushClipRect(physicalBounds);
it.addPicture(ui.Offset.zero, picture);
it.pop();
ui.window.render(it.build());
};
}
```
Contributor guide
Assessment
This issue has not been assessed yet.