Skip to content

Anonymous methods #260

Open
Open
@eernstg

Description

@eernstg

In response to #259, this issue is a proposal for adding anonymous methods to Dart.

Edit: We could use the syntax e -> {...} in order to maintain syntactic similarity with the pipe operator proposed as #43, as mentioned here. See #265 for details. Later edit: Changed generic anonymous methods to be a separate extension of this feature, such that it is easier to see how it works without that part.

Examples

The basic syntax of an anonymous method is a term of the form .{ ... } which is added after an expression, and the dynamic semantics is that the value of that expression will be "the receiver" in the block, that is, we can access it using this, and methods can be called implicitly (so foo(42) means this.foo(42), unless there is a foo in the lexical scope).

To set the scene, take a look at the examples in #259, version 1 (original), 2 (unfolded), and 3 (folded). Here is a version of that same example that we could write using anonymous methods:

// Version 4, using anonymous methods.

void beginFrame(Duration timeStamp) {
  // ...
  ui.ParagraphBuilder(
    ui.ParagraphStyle(textDirection: ui.TextDirection.ltr),
  ).{
    addText('Hello, world.');
    build().{
      layout(ui.ParagraphConstraints(width: logicalSize.width));
      canvas.drawParagraph(this, ui.Offset(...));
    };
  };
  ui.SceneBuilder().{
    pushClipRect(physicalBounds);
    addPicture(ui.Offset.zero, picture);
    pop();
    ui.window.render(build());
  };
}

In this version the emphasis is on getting the complex entities (the objects that we are creating and initializing) established at first. With the given ParagraphBuilder in mind, we can read the body of the anonymous method (where we add some text to that paragraph builder and build it). We continue to work on the paragraph returned by build, doing a layout on it, and then using it (this) in the invocation of drawParagraph.

Similarly, the second statement gets hold of the scene builder first, and then works on it (with three method invocations on an implicit this, followed by one regular function call to render).

Compared to version 3, this version essentially turns the design inside out, because we put all the entities on the table before we use them, which allows drawParagraph and render to receive some much simpler arguments than in version 3.

Compared to version 2, this version allows for a simple use of statements, without the verbosity and redundancy of using names like paragraphBuilder many times. This means that we can use non-trivial control flow if needed:

ui.SceneBuilder().{
  pushClipRect(physicalBounds);
  for (var picture in pictures) {
    addPicture(randomOffset(), picture);
  }
  pop();
  ui.window.render(build());
};

We can of course also return something from an anonymous method, and we can use that to get to a sequential form rather than the nested one shown above:

ui.ParagraphBuilder(
  ui.ParagraphStyle(textDirection: ui.TextDirection.ltr),
).{
  addText('Hello, world.');
  return build();
}.{
  layout(ui.ParagraphConstraints(width: logicalSize.width));
  canvas.drawParagraph(this, ui.Offset(...));
};

We can choose to give the target object a different name than this:

main() {
  "Hello".(that){ print(that.length); };
}

We can use a declared type for the target object in order to get access to it under a specific type:

main() {
  List<num> xs = <int>[];
  xs.(Iterable<int> this){ add(3); }; // Dynamic check.
}

Note that this implies that there is an implicit check that xs is Iterable<int> (which may be turned into a compile-time error, e.g., by using the command line option --no-implicit-casts). Also, the addition of 3 to xs is subject to a dynamic type check (because xs could be a List<Null>).

As a possible extension of this feature, we can provide access to the actual type arguments of the target object at the specified type:

main() {
  List<num> xs = <int>[];
  xs.(Iterable<var X> this){ // Statically safe.
    print(X); // 'int', not 'num'.
    num x = 3; // Note that `xs.add(x)` requires a dynamic check.
    if (x is X) add(x); // Statically safe, no dynamic checks.
  };
}

We are using type patterns, #170, in order to specify that X must be bound to the actual value of the corresponding type argument for xs. We also specify that X must be a subtype of num, such that the body of the anonymous function can be checked under that assumption, and this is a statically safe requirement because the static type of xs is List<num>.

Finally, if the syntax works out, we could extend this feature to provide a new kind of function literals. Such a function literal takes exactly one argument which is named this, and the body supports implicit member access to this, just like an instance method and an anonymous method:
use an anonymous method as an alternate syntax for a function literal that accepts a single argument (and, of course, treats that argument as this in its body):

main() {
  List<String> xs = ['one', 'two'];
  xs.forEach(.{ print(substring(1)); }); // 'ne', 'wo'
}

Proposal

This is a draft feature specification for anonymous methods in Dart.

Syntax

The grammar is modified as follows:

<cascadeSection> ::= // Modified
    '..'
    <cascadeHeadSelector>
    <cascadeTailSelector>*
    (<assignmentOperator> <expressionWithoutCascade>)?

<cascadeHeadSelector> ::= // New alternative added
    <cascadeSelector> <argumentPart>*
  | <anonymousMethod>

<cascadeTailSelector> ::= // New alternative added
    <assignableSelector> <argumentPart>*
  | <anonymousMethodSelector>

<selector> ::= // New alternative added
    <assignableSelector>
  | <anonymousMethodSelector>
  | <argumentPart>

<anonymousMethodSelector> ::= // New
    '.' <anonymousMethod>
  | '?.' <anonymousMethod>

<anonymousMethod> ::= // New
    <typeParameters>? ('(' <normalFormalParameter> ')')? <block>

<simpleFormalParameter> ::= // Modified
    <declaredIdentifierOrThis>
  | 'covariant'? <identifierOrThis>

<identifierOrThis> ::= <identifier> | 'this'

<declaredIdentifierOrThis> ::=
    'covariant'? <finalConstVarOrType> <identifierOrThis>

Static Analysis

It is a compile-time error if a formal parameter is named this, unless it is a parameter of an anonymous method or a function literal.

An anonymous method invocation of the form e.{ <statements> } or e..{ <statements> } is treated as e.(T this){ <statements> } respectively e..(T this){ <statements> }, where T is the static type of e.

An anonymous method invocation of the form e?.{ <statements> } is treated as e?.(T this){ <statements> } where T is the static type of e (with non-null types, T is the non-null type corresponding to the static type of e).

The rules specifying that an expression e starting with an identifier id is treated as this.e in the case where id is not declared in the enclosing scopes remain unchanged.

However, with anonymous methods there will be more situations where this can be in scope, and when an anonymous method is nested inside an instance method, the type of this will be the type of the receiver of the anonymous method invocation, not the enclosing class.

In an anonymous method invocation of the form e.(T id){ <statements> } or e..(T id){ <statements> }, it is a compile-time error unless the static type of e is assignable to T. (Note that id can be this.) Moreover, it is a compile-time error if T is dynamic.

It is a compile-time error if a statement of the form return e; occurs such that the immediately enclosing function is an anonymous function of the form e..(T id){ <statements> }. This is because the returned value would be ignored, so the return statement would be misleading.

The static type of an anonymous method invocation of the form e.(T id){ <statements> } is the return type of the function literal (T id){ <statements> }. The static type of e?.(T id){ <statements> } is
S?, where S is the return type of the function literal (T id){ <statements> }.

The static type of an anonymous method invocation of the form e..(T id){ <statements> } is the static type of e.

Dynamic Semantics

Evaluation of an expression of the form e.(T id){ <statements> } proceeds as follows: e is evaluated to an object o. It is a dynamic error unless the dynamic type of o is a subtype of T. Otherwise, (T id){ <statements> }(o) is evaluated to an object o2, and o2 is the result of the evaluation.

Evaluation of an expression of the form e?.(T id){ <statements> } proceeds as follows: e is evaluated to an object o. If o is the null object then the null object is the result of the evaluation, otherwise it is a dynamic error unless the dynamic type of o is a subtype of T. Otherwise, (T id){ <statements> }(o) is evaluated to an object o2, and o2 is the result of the evaluation.

Evaluation of an expression of the form e..(T id){ <statements> } proceeds as follows: e is evaluated to an object o. It is a dynamic error unless the dynamic type of o is a subtype of T. Otherwise, (T id){ <statements> }(o) is evaluated to an object o2, and o is the result of the evaluation.

Discussion

As mentioned up front, we could use -> rather than . to separate the receiver from an associated anonymous method, which would make this construct similar to an application of the pipe operator (#43).

This might be slightly confusing for the conditional variant (where we would use ? -> rather than ?.) and the cascaded variant (where we might use --> rather than .., and ? --> rather than ?.., if we add null-aware cascades).

It might be useful to have an 'else' block for a conditional anonymous method (which would simply amount to adding something like ('else' <block>)? at the end of the <anonymousMethodSelector> rule), but there is a syntactic conflict here: If we use else then we will have the combination of punctuation and keywords (e?.{ ... } else { ... } ). Alternatively, if we use : then we get a consistent use of punctuation, and we get something which is similar to the conditional operator (b ? e1 : e2), but it will surely surprise some readers to have : as a larger-scale separator (in e?.{ ... } : { ... }, the two blocks may be large).

Note that all other null-aware constructs could also have an 'else' part, specifying what to do in the case where the receiver turns out to be null, such that the expression as a whole does not have to evaluate to the null object.

Note that we could easily omit support for this parameters in function literals, or we could extend the support to even more kinds of functions.

We insist that the receiver type for an anonymous method cannot be dynamic. This is because it would be impractical to let every expression starting with an identifier denote a member access on that receiver:

main() {
  dynamic d = 42;
  d.{ print(this); }; // Oops, `42` does not have a `print` method!
}

However, another trade-off which could be considered is to allow a receiver of type dynamic, but give it the type Object in the body.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureProposed language feature that solves one or more problems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions