Sdk: Add first class support for method forwarding

Created on 29 Nov 2017  路  5Comments  路  Source: dart-lang/sdk

It's a common pattern to wrap an interface with a Delegating class which forwards all methods to a instance of the same interface and allows selectively overriding methods.

For example see Delegating* classes in package:collection and package:async

It would be cool if this was automatic rather than needing to manually write these classes.

Here's a straw man proposal:

abstract class SomeInterface {
  String someMethod();
  int someOtherMethod();
}

class Foo implements SomeInterface {
  final delegate SomeInterface _delegate;
  Foo(this._delegate);

  @override
  String someMethod() => 'Foo implementation';

  // Implicitly:
  // int someOtherMethod() => _delegate.someOtherMethod();
}

If:

  • a field is marked delegate
  • The delegate field is statically determined to be of type Foo
  • The class implements Foo

Then:

  • Any methods in Foo which are not directly implemented in the class are automatically implemented by forwarding to the field marked delegate.

I think this also could work with multiple delegates and it would be a static error to have a method with identical signatures in two interfaces with ambiguous forwarding. This is similar to a static error when implementing two interfaces with the same method and conflicting signatures.

class Foo implements First, Second {
  final delegate First _first;
  final delegate Second _second;
  Foo(this._first, this._second);
}
area-language core-m type-enhancement

Most helpful comment

It's an interesting idea that I've been thinking about for a while.

There is a difference between forwarding and delegation. In the latter, the this object during the call will be the original object, not the one the call is forwarded to (like if it was an inherited superclass method, not a separate object). I don't think it's likely that we'll get proper delegation in Dart, it's too big a change, and it would prevent some optimizations that rely on knowing the type of this.

Forwarding is easier, it's just implicit forwading methods that you would have to write yourself otherwise. (and if we had forwarding syntax for methods, like for constructors, it would be even easier, then it really would just be an implciit foo(arg1, arg2) = _theFoo.foo;.
The biggest challenge is finding a syntax for linking the interface with the object, and handling conflicts.

class D {
  int foo() => 42;
}
class C implements D {
  delegate D =>_theD;
  D get _theD => ...
  ...
  // implicit:  int foo() => _theD.foo();
}

or

class C implements D as _theD {
  get D _theD => ...;
  ...
  // implicit:  int foo() => _theD.foo();
}

or something else.

Linking it up during construction is restrictive - you can't change the forwarded-to object while running (well, you can, you just have to work harder for it, so the restrictions are probably not buying much).

You need to handle conflicts. If you have two delegates to related interfaces (anything with common non-trivial supertype), you have two figure out where the conflicted methods go. Most likely, it would just be an error, and you'll have to handle those methods manually (If you delegate both List and Queue, the Iterable methods won't know where to go).

Maybe if one delegated type is a subtype of another, that one always win., and if you have multiple, one must be a subtype of all the rest. Then you can do:

class C implements Set<int>, List<int> {
  delegate Set<int> => theSet;
  delegate List<int> => theList;
  delegate Iterable<int> => _theSet;  // Solve conflict between Set and List.
  Set<int> get _theSet => ...;
  List<int> get _theList => ...;
}

As forwarding, it's pure syntactic sugar, but it's adaptive syntactic sugar. If the interface gets more members, you don't have to go back and add the extra methods to your forwarding wrappers. I've written enough such forwarding classes to consider that valuable.

All 5 comments

It might be simpler to encapsulate at a higher level:

class Foo implements First, Second {
  delegate factory Foo(First first, Second second);
}

It would be simple enough to prototype this, for example, using source_gen:

part 'foo.g.dart';

abstract class Foo implements First, Second {
  @delegate factory Foo(First first, Second second) = _$Foo$Delegate;
  Foo._();
}

It's an interesting idea that I've been thinking about for a while.

There is a difference between forwarding and delegation. In the latter, the this object during the call will be the original object, not the one the call is forwarded to (like if it was an inherited superclass method, not a separate object). I don't think it's likely that we'll get proper delegation in Dart, it's too big a change, and it would prevent some optimizations that rely on knowing the type of this.

Forwarding is easier, it's just implicit forwading methods that you would have to write yourself otherwise. (and if we had forwarding syntax for methods, like for constructors, it would be even easier, then it really would just be an implciit foo(arg1, arg2) = _theFoo.foo;.
The biggest challenge is finding a syntax for linking the interface with the object, and handling conflicts.

class D {
  int foo() => 42;
}
class C implements D {
  delegate D =>_theD;
  D get _theD => ...
  ...
  // implicit:  int foo() => _theD.foo();
}

or

class C implements D as _theD {
  get D _theD => ...;
  ...
  // implicit:  int foo() => _theD.foo();
}

or something else.

Linking it up during construction is restrictive - you can't change the forwarded-to object while running (well, you can, you just have to work harder for it, so the restrictions are probably not buying much).

You need to handle conflicts. If you have two delegates to related interfaces (anything with common non-trivial supertype), you have two figure out where the conflicted methods go. Most likely, it would just be an error, and you'll have to handle those methods manually (If you delegate both List and Queue, the Iterable methods won't know where to go).

Maybe if one delegated type is a subtype of another, that one always win., and if you have multiple, one must be a subtype of all the rest. Then you can do:

class C implements Set<int>, List<int> {
  delegate Set<int> => theSet;
  delegate List<int> => theList;
  delegate Iterable<int> => _theSet;  // Solve conflict between Set and List.
  Set<int> get _theSet => ...;
  List<int> get _theList => ...;
}

As forwarding, it's pure syntactic sugar, but it's adaptive syntactic sugar. If the interface gets more members, you don't have to go back and add the extra methods to your forwarding wrappers. I've written enough such forwarding classes to consider that valuable.

There is a difference between forwarding and delegation. In the latter, the this object during the call will be the original object, not the one the call is forwarded to (like if it was an inherited superclass method, not a separate object).

Thanks for the clarification! By these terms forwarding is what I'd hope for and I'd explicitly not want delegation.

Some times ago I wrote zengen that contains a generator to do _delegation_/_forwarding_. The annotation had an optional parameter to be able to exclude some methods and provide a way to manage conflicts between delegates.

Wow that zengen is useful, I was writting a wrapper for a flutter class of over 30 methods. Thanks. I hope they can incorporate this delegate syntantic sugar (being the simplest implementation) as soon as possible.

Was this page helpful?
0 / 5 - 0 ratings