Skip to content

Automatically generating missing methods #631

Open
@eernstg

Description

@eernstg

This issue is a proposal for supporting a simple automatic class member generation feature. It would enable automatic generation of a set of instance members of a class with an implementation that is structurally identical for sets of several methods. Being a language mechanism, it would not involve a separate code generation step, nor any specialized tools: the code generation would be performed by the front end such that all tools (analyzer, compilers, runtime) would see the generated code as-if written manually.

For instance, it could support forwarding by implementing all members of a given interface such that they call forwardee.g as the implementation of a getter named g, forwardee.m(a1) as the implementation of a method m taking one argument, etc.

This mechanism is not intended to support a more elaborate static meta-programming system where each generated method could have an implementation which is computed via a powerful meta-level computation. For instance, with this example in D, setField has a body that contains a switch where the list of cases is computed based on the members of the given type denoted by Object. That's a non-goal for this proposal.

However, this mechanism does allow for automatically and concisely obtaining (and maintaining) a set of member implementations that have a syntactically nearly identical implementation, and that's a kind of feature that Dart does not have currently.

Static Meta-programming

The need for some amount of static meta-programming has come up many times in Dart language debates. For example, #418 requests better support for proxy objects, #370 and #493 requests a concise syntax for passing all parameters from one constructor to another one, #582 mentions property delegation, many requests have been made for concise declarations of immutable classes, etc. It would be possible to use an approach based on static meta-programming for all of these requests (and, of course, there are also other approaches).

Static meta-programming as in Rust would be great, but that is a large feature, and it may not fit well into Dart because the languages are so different.

However, in one particular area, static meta-programming may be a low-hanging fruit for Dart: We can re-use the machinery known as 'noSuchMethod forwarders'. This is a proposal to do just that.

Rely on noSuchMethod Forwarding

In general, noSuchMethod is invoked whenever an instance member invocation is attempted, but no implementation of the requested kind (method, getter, setter) and with the given name exists.

In order to enforce normal typing constraints (and for performance reasons), this feature has been implemented using a mechanism known as noSuchMethod forwarders: In a concrete class with an implementation of noSuchMethod different from the one in Object, a member is generated for every member in the interface of the enclosing class that has no implementation.

This proposal re-uses that approach: Let C be a concrete class with interface I. Currently, it is an error unless C declares or inherits an implementation of every member in I. However, this proposal allows C to declare a method template, a getter template, and a setter template, and they are then used to generate code for any such missing implementations.

Templates

The member templates would have the following form:

// Method template.
<targets>? template R name(P) ...

// Getter template.
<targets>? template R get name ...

// Setter template.
<targets>? template void set name(P) ...

The body of each template is represented as .... It would be a regular function body, possibly using R, P, and name. These meta-names are detected based on the syntactic form of the template, so with template Ret m(Parameters) ... they would be Ret rather than R, and so on.

The <targets> part can be used to specify that the template is used for specific member names or specific interfaces. It would be a comma separated list where each element is an <identifier> or a <typeName>. For instance, I template R get name => throw "Not supported"; would give rise to generation of throwing implementations of all otherwise unimplemented members of I. A template matches a given member if it has the right kind (method, getter, setter), and if either its basename occurs in the target list, or the target list contains a type name that denotes an immediate superinterface whose interface contains that member. If multiple templates match a given member then the one that occurs textually first will be used.

The body is subject to normal parsing, except that the meta-name that denotes the parameters in a method can only be used as an actual argument list. The setter does not have this restriction: P in the setter above expands to the name of the parameter in the body, which is just an expression.

Code generation for a method named m replaces name by m, R by the return type of m in the interface of the class, and P by the list of formal parameters as arguments. (So when generating code for void foo(int i, {bool b = false}), bar(P) in the body would expand to bar(i, b: b).) Operators are methods, and are treated as such. Getters and setters are treated similarly as methods.

The code generation step for a class iterates over all methods in the interface of the given class, and performs code generation for each member that has no implementation. (In particular, for any specific unimplemented member where we do not wish to have that implementation, we simply write a normal member declaration.) If a member m is not implemented, and it is a method, and there is no method template, a compile-time error occurs. (This is not new, it simply occurs because the class is concrete, but does not fully implement its interface.) Similarly for getters and setters.

The code generation step for a mixin iterates over all members that occur in the combined implements interface, and not in the on interface, nor in the body of the mixin itself, and otherwise works the same as with a class.

A class or mixin with templates may contain all the regular kinds of member declarations, including regular instance member declarations, and static methods and variables.

Example

Consider the case where we wish to create a class that holds a final reference to some other object, forwardee, and forwards all invocations to forwardee:

class Cforwarder implements C {
  final C forwardee;
  C_as_I(this.forwardee);

  template R name(P) => forwardee.name(P);
  template R get name => forwardee.name;
  template void set name(P) => forwardee.name = P;
}

Consider the case where the class Foo does not implement an interface Bar, but with suitable imports it is possible to get access to extension methods such that every member of Bar can be invoked, with some invocations calling instance methods of Foo and others calling some extension method. Except foo—which is just an example, intended to show how to provide a hand-written implementation of any additional members if needed.

We can bridge the gap by generating code that will perform all these invocations in a setting where the extension methods are available, and then we'll get a wrapper object that actually implements Bar, providing the oddball foo method manually:

class FooImplementingBar implements Foo, Bar {
  final Foo forwardee;
  FooImplementingBar(this.forwardee);

  // This is a normal member declaration.
  void foo(String s) { ... }

  template R name(P) => forwardee.name(P);
  template R get name => forwardee.name;
  template void set name(P) => forwardee.name = P;
}

mixin FooImplementingBarMixin on Foo implements Bar {
  Foo get forwardee; // Must be implemented by subclass.

  // This is a normal member declaration.
  void foo(String s) { ... }

  template R name(P) => forwardee.name(P);
  template R get name => forwardee.name;
  template void set name(P) => forwardee.name = P;
}

In the case where different sets of members require a different template, it is possible to split the templates using targets:

abstract class A { void foo1(); int foo2(int i); }
abstract class B { int get bar; int get baz; }

class C1 implements A, B {
  final A a = A();

  A template R name(P) => a.name(P);
  // Yields the following:
  //   void foo1() => a.foo1();
  //   int foo2(int i) => a.foo2(i);

  B template R get name => 42;
  // Yields the following:
  //   int get bar => 42;
  //   int get baz => 42;
}

Enhancements

Obviously, it would be possible to generalize the templates in a thousand ways. However, keeping them minimal would allow us to get this feature with a moderate amount of work, and it would still be rather powerful.

Conversely, it would be easy to allow the parameter meta-variable in a method to be used in other syntactic locations where a list of expressions is allowed, and make it a compile-time error if there are any named parameters, or simply omitting them if the parameter meta-variable is not used as an actual argument list of a function call. This would make it possible to create a list literal containing all the parameters. If all parameters are named then it might be possible to use them to obtain a map literal. Similarly, we could allow f(x, P, y: 42) to denote an invocation of f that passes a longer list of actual arguments than the one which is denoted by the meta-variable P.

It would require a more involved approach to meta-syntax if we were to allow for more elaborate patterns. For instance, a template could specify that only certain return types match, or only certain parameter list shapes match, and that template would then be skipped for non-matching members:

abstract class I { int foo(int i); String bar(); }

class C implements I {
  template (R:int) name(P) => ...; // Matches `foo` and not `bar`.
  template R name() => ...; // Matches `bar` and not `foo`.
}

The name of a member could be made available as a symbol or as a string literal, which could be relevant for situations like the ones discussed in #251. We could use special "functions" like stringOf(name) and symbolOf(name) to obtain such strings or symbols.

It could also be useful to declare that some or all members should be generated, even if they are already implemented:

class C ... // Some useful class out there.

class LoggingC extends C {
  // Suppose we can use `override` to request generation of an implementation
  // when and only when an implementation of that member is inherited.
  override template R name(P) {
    print("Invoking ${stringOf(name)}.");
    return super.name(P);
  }
  // And similarly for getters and setters.
}

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