Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keyword to easily forward function calls proxy/delegate, mirrors intermediate #418

Open
icatalud opened this issue Jun 23, 2019 · 15 comments
Labels
request Requests to resolve a particular developer problem

Comments

@icatalud
Copy link

I want to be able to easily delegate an object's function call to another object, like a proxy. A way I think this could be nicely implemented and safe for static analysis, is the one described here.

I believe that this functionality would provide a solution to an important part of the users that for are requesting mirrors for flutter. Many people want mirrors just to proxy function calls. It would also provide a much more general solution to a previous request about object wrappers/proxies dart-lang/sdk#414.

The idea is to introduce a keyword 'delegate' that repeats the original call made to a class method to another object. Some positive things about introducing this would be:

  • Syntactic sugar for some common patterns, like calling super.methodName(args...)
  • Introduces a subset of mirrors functionality, but static type safe
  • Easy creation of proxy/wrapper classes

Details are clarified in the examples below. What do you think?

(is it more clear write simpler example with class A, B, bar, foo, etc?)

class House {
  bool setEncompassingLimits(northLimit, soutLimit, westLimit, eastLimit) { /* ... */ }
  // ...
}

class TreeHouse extends House {
  @override
  bool setEncompassingLimits(northLimit, southLimit, westLimit, eastLimit) {
    // ...
    delegate super; // equivalent to super.setEncompassingLimitss(args...) with original function call args
    // ...
  }

  cleanTreeBranches(tool) {}
}

class HouseManager {
  House house;

  bool setEncompassingLimits(args) => delegate house;
  //....
}

TreeHouse treeHouse;
HouseManager houseManager;

treeHouse.setEncompassingLimits(args...); // will call super.setEncompassingLimits(args...) 

houseManager.setEncompassingLimits(args...); // will call houseManager.house.setEncompassingLimits(args...)

Essentially 'delegate' means, repeat the exact same call that was done in the current object on another object (or super). The object must therefore implement the exact same function otherwise it is a an undefined_method error. This is syntactic sugar for a common pattern that is calling super.methodName(args...), but is more versatile than that by allowing to repeat the call to another object, which is also used sometimes, specially in "proxy" classes.

Special Case noSuchMethod

A special case is calling 'delegate' on noSuchMethod. When delegate is called inside a noSuchMethod function declaration, the call of the function to super or another object should be of the original function that was called and not to noSuchMethod. For example:

class HouseHolder implements TreeHouse {
  House house;
  TreeHouse treeHouse;

  noSuchMethod(Invocation invocation) => {
    delegate house;
    delegate treeHouse;
  };
  // error in this class definition, HouseHolder must implement cleanTreeBranches as explained below
}

HouseHolder houseHolder;

houseHolder.setEncompassingLimits(args...) // allowed, makes same function call in house and treeHouse

houseHolder.cleanTreeBranches(args...) // static type check error, house does not implement cleanTreeBranches

If a method is on the interface of the class but is not implemented, the static type checks should assume a call of methodName(args...) to each object where the call is being delegated inside noSuchMethod. In other words, the implemented interface reached by noSuchMethod is equal to the intersection of the interfaces of the delegated class instances, in the above example it would be House and TreeHouse interfaces intersection.

Small optimizations

It could also be allowed to skip the function body and do something like this:
methodName(args...): delegate another_object;

Which the compiler can immediately interpret as a call another_object.methodName(args...), without passing through the method that delegates. Like this:

class HouseProxy implements TreeHouse {
  House house;
  TreeHouse treeHouse;

  cleanTreeBranches(tool): delegate treeHouse; // compiler send call to this method directly to treeHouse
  noSuchMethod(Invocation invocation): delegate house; // the compiler forwards any call of the House interface directly to house
}

HouseProxy proxy;

proxy.cleanTreeBranches(tool)  // compiler understands this is equivalent to proxy.treeHouse.cleanTreeBranches(args...)

proxy.setEncompassingLimis(args...)  // compiler understands this is equivalent to proxy.house.setEncompaasingLimits(args...)
@lrhn
Copy link
Member

lrhn commented Jun 24, 2019

There are several requests in this issue (explict or implicit).

  • Forwarding individual methods calls without repeating arguments.
  • Forwarding methods to other methods.
  • Forwarding all methods of an interface to another objcet.
  • Forwarding noSuchMethod invocations.

As always with language features, the difficulty lies in finding a good tradeoff between generality and complexity. Making something very specific makes it easier to introduce, but also more likely to not be enough to solve as many problems.

The delegate super; would invoke the current instance method on the super-class, with the same arguments. That obviously only works when the super-class has such a method and it accepts the exact same arguments. It avoids writing super.methodName(argName1, ..., argNameN) (plus even more if parameters are named). It feels a little too restricted to me, only applying in very specific cases, and not generalizing at all. Still, calling super.sameMethod(sameArguments) likely does happen more than most other super-invocations, it's a way to implement the decorator pattern.

Forwarding to another method: : delegate house would forward the current method call to the same named method on house, which must accept the same arguments. We already have forwarding constructors, so maybe we can use some similar notation. Instead of only forwarding to the same method, maybe we could specify the method and have general function forwarding: printInt(int x) = print; or String operationOnHouse(String something) = house.operationOnHouse;. The expression after the = can be any function-typed expression with a compatible function type. If it denotes a method, the forwarding doesn't have to tear off the method before calling it, giving the optimization. This allows you to omit parameters when simply forwarding them to another function. That is:

 int something(args) = expression;
 // is equivalent to
 int something(args) => expression(args);

I think that could be very useful.

Forwarding an entire interface: Instead of relying on noSuchMethod to call another class, perhaps we could designate specific getters as proxies for their interface. Something like proxy House house; would mean that all members of House which are not overridden by the current class declaration, are introduced and forwarded to the same member on house. So:

class TreeHouse implements House {
  final proxy House _house;
  final Tree tree;
  TreeHouse(House house, Tree tree) : this._house = house, this.tree = tree;
   int get height => _tree.height + _house.height;  
   // all members of House are forwarded to _house.
}

Usual caveats apply - forwarded members count as declared in the forwarding class, so two forwardings with the same name will conflict. You'll have to do that forwarding manually, as with height above.

Forwarding noSuchMethod invocations: If a noSuchMethod receives an invocation which applies to a specific type, calling the invocation's method and arguments on something with that type would be convenient. It would also be new functionality. There currently is no way to do that without knowing the interface. Something like static Object invokeOn(Object receiver, Invocation invocation) could do the job. It would check the member name of the invocation, and do a dynamic lookup of that name on recevier, and then perform the invocation if it is valid. (What happens if it isn't?).
This is mirror-like behavior, and it would affect compilers' ability to tree-shake. It's probably not going to happen.

@natebosch
Copy link
Member

Some discussion of a similar idea I head: #3748

@icatalud
Copy link
Author

icatalud commented Jun 24, 2019

@Updated

Some discussion of a similar idea I head: #3748

@natebosch Yes, thank you for posting here. I think your solution is exactly what @lrhn wrote as 'proxy'. Perhaps 'proxy' is a more suitable word, understating JS uses it. Doesn't that request belong to language?

As always with language features, the difficulty lies in finding a good trade-off between generality and complexity. Making something very specific makes it easier to introduce, but also more likely to not be enough to solve as many problems.

I would also add that more complex features are also sometimes less intuitive and harder to understand. I tried to approach all the the points you listed with one keyword that behaves with slight differences depending on the context, which might be more confusing than adding separate functionality for different concerns, like some suggestions you made.

The feature I'm missing the most is proxies. The 'proxy' functionality you and natebosch proposed is actually exactly what would solve the current requirement that inspired me to make this proposal. If you could include such feature, it would be extremely useful. That said, let me explain a few details about what I intended to propose.

First
You have a keyword 'delegate x' or 'forward x' that when used inside a function definition 'exampleMethod' is syntactic sugar for doing x.exampleMethod(args...). Just like you pointed it out, this is not the most general approach, because it forces the same function name. In that sense it is better to delegate directly to the callable an not the same method definition in an object, like 'delegate x.exampleMethod'. However that would interfere with the potential proxy capability. For now let's assume the initial syntactic sugar behavior I explained.

Proxying with noSuchMethod

Something like static Object invokeOn(Object receiver, Invocation invocation) could do the job. It would check the member name of the invocation, and do a dynamic lookup of that name on recevier, and then perform the invocation if it is valid. (What happens if it isn't?).

I know you do not want to have such method, because that violates static type checks. What I proposed is something that is a little bit ugly (at least so it seems initially, but it is actually intuitive), because it seem to violate the idea that functions and statements behave the same everywhere. That would be definitely confusing and undesired, but perhaps worth it if it adds something that's useful. Currently noSuchMethod behaves differently from other methods both for static type checks and for program behavior. If someone is new to the language and implements noSuchMethod, they might ask "why is this method suddenly being called when I attempt to call a method that is not declared?" The program should crash. And then you reply that you defined the language like that, you included special behavior for that method with the intention of giving more flexibility to the developers. Similarly if you implement noSuchMethod on a class that implements an interface, suddenly you do not have to define any method of the interface and the static type checks pass. Weird eh? I thought to myself "thank god you included those features" when I learned the basics of dart (I don't care if it weird from a purist standpoint).
Well, what I attempted to say on the original proposal is that since noSuchMethod is already "magical", behaving differently from other methods, what if you overload it with a little bit more of special behavior, particularly if this keyword 'delegate' was introduced.

There is an intuitive way of understanding 'delegate', which is the following: 'delegate' repeats the last written (in dart code) function call in the supplied var. In code debug steps the following happens when you call a method on a class that implement noSuchMethod:

  1. instance.exampleMethod(args...); // last dart written function call statement
    // dart internally interprets previous call, notices instance does not implements exampleMethod and does some stuff under the hood
  2. instance.noSuchMethod(invocation) // for some magical reason the call derives here
  3. delegate x; // last dart function call statement was point 1, therefore delegate x is syntactic sugar for x.exampleMethod(args...)

That way it makes sense right? It's not ugly if you understand it like that. noSuchMethod can itself be delegated, when there is an explicit call to it, like instance.noSuchMethod(invocation).

It's very easy to do static checks if that is the case, because for example:

class A {
  exampleMethod(args...) {}
}

class B implements A {
  A a;
  C c;
  noSuchMethod(...) {
    //...
    delegate a;
    delegate c;
  }
}

B b = B();
b.exampleMethod(); // should check if A and C implement exampleMethod

Since static checks see noSuchMethod implemented on B, it should also check the noSuchMethod own "interface". This means checking for the interfaces of all delegates inside noSuchMethod. Another way of looking at this, is that calling b.exampleMethod is "equivalent" (for type checking purposes) of doing:

b.exampleMethod()
<=>
b.exampleMethod() // yes because B implements noSuchMethod
a.exampleMethod() // yes because A implements exampleMethod
c.exampleMehotd() // depends on whether C implements exampleMethod or noSuchMethod

A small caveat is that delegates inside if-else or for statements are considered like forwards in the main block for type checks (it doesn't make a difference).

The inclusion of such feature would provide a solution to all points you made, except forward method to other methods.

About your inputs

The advantages I see in your ideas are the separation of concern that allows a wider spectrum of usages to be solved and perhaps in a more intuitive way (in a world where the 'delegate' word has not been incorporated yet).

int something(args) = expression;
// is equivalent to
int something(args) => expression(args);

This takes on forwarding to any function that takes same args, it's also more intuitive in terms of the current vocabulary. But it doesn't give you the option to write a function body, like a decorator pattern.

final proxy House _house;

The usage of 'proxy' seems in principle more clear, but in a way it is more obscure. In the world of dart people know already that undefined methods derive to noSuchMethod. If you allowed a definition like "noSuchMethod(...): delegate x" it would make people wonder what the hell is going on, but at least they would arrive there immediately and then go check the docs to understand what is happening. It keeps the trend "if there is something unexpected happening, check for noSuchMethod, that's where dart magic is". Including a proxy var at the beginning does not immediately raise suspicions that it would be that statement what is causing the class to behave unexpectedly.

@lrhn
Copy link
Member

lrhn commented Jun 25, 2019

About invokeOn(target, invocation)

I know you do not want to have such method, because that violates static type checks.

That is not why I don't want it. We have dynamic invocations and Function.apply too, and it wouldn't violate anything, it'f just fail if the types are wrong.

The reason I don't want it is that it makes it harder to analyze the program. If you have an invokeOn(object, someInvocation) anywhere in the program, then the compiler has to assume that any function on any object can be invoked. (Well, at least if you also create an Invocation object that the compiler can't deduce the name of).

If you don't have such a reflective invocation, and there is no invocation of a method named foo on a type assignable to Foo anywhere in the program, then the Foo.foo method can be tree-shaken.
Even if there are dynamic invocations, if they are not dynamicVar.foo, then Foo.foo is still not reachable by the program. The fact that you write .foo nowhere in the program can actually be used for something.

The moment you allow a dynamic selector, where you don't know the method name at compile-time, you risk increasing the size of ahead-of-time compiled programs by a lot. For web deployment, that would be a strong anti-feature, so you quickly see style guides recommending that you never use the feature. Spending time on such a feature seems like a bad idea then.

Now, if you don't care about that, you can just use dart:mirrors, and I'm sure you can already implement invokeOn using dart:mirrors, so it's not something we need to add.

I'm opposed to adding reflective features outside of dart:mirrors because they come with a real cost to compilers.

This brings us back to noSuchMethod/delegate x:

Using delegate x; as a statement inside a noSuchMethod function,
to effectively mean:

return x.<invocation.memberName>(<invocation args>);

can definitely be made to work.
The main problem is that it depends on the invocation object,
which is just an object that anyone can create.
There is no way to analyze this code statically, the forwarding happens
entirely dynamically because it depends on the Invocation object.

It allows me to declare an invokeOn function as:

class _CallIt {
  final _receiver;
  _CallIt(this._receiver);
  dynamic noSuchMethod(Invocation i) {
    delegate _receiver;
  }
} 

dynamic invokeOn(Object receiver, Invocation invocation) =>
    _CallIt(receiver).noSuchMethod(invocation);

// or even:
dynamic invokeMethod(Object receiver, Symbol name, 
   [List<Object> args = const <Object>[],
    Map<Symbol, Object> named = const <Symbol, Object>{}]) =>
   _CallIt(receiver).noSuchMethod(Invocation.method(name, args, named));

By allowing a user-supplied invocation to be acted on reflectively, we add
reflective functionality to the core language. Again, this costs.

If you couldn't invoke noSuchMethod directly, then it would be slightly safe,
but you can, and you do in some cases (mainly to call super.noSuchMethod).

I know it's convenient, it allows mocks and decorators to be simple and general,
and not know the members they are forwarding. Sadly, it also means that the
compiler won't know what those classes are doing either, because it's an
entirely dynamic forwarding.

@icatalud
Copy link
Author

icatalud commented Jun 25, 2019

That is not why I don't want it. We have dynamic invocations and Function.apply too, and it wouldn't violate anything, it'f just fail if the types are wrong.

Apologizes, I didn't express my self correctly. I said violate static type checking, but I was thinking on what you call static analysis. You do not lose any static analysis capabilities if you apply the definition I gave before. I believe I caused confusion in the explanation by stating that noSuchMethod could be overloaded with more special functionality on delegate, but it's not really the case (depends on the point of view).

Let's walk through your example according to the following definition:

'delegate' repeats the last static typed function call that derived into the current function block on the supplied var.

(imagine there is a stack of the static typed function calls)

By using that definition, if a user called invokeOn(object, invocation), the following steps would happen:

  1. invokeOn(object, invocation) // function call
  2. _CallIt(receiver).noSuchMethod(invocation); // function call
  3. delegate _receiver;
  4. object.noSuchMethod(invocation) // because the last function call was noSuchMethod(invocation)

That would happen according to the definition of 'delegate' provided above. On the other hand, if you do the following:

  1. callIt = _CallIt(object)
  2. callIt.someMethod(args...) // function call
  3. callIt.noSuchMethod(invocation) // for unknown-uncontrolled reasons somehow the program derives previous function call to this method although statically this was never typed, it does not count as a function call
  4. delegate _receiver;
  5. object.someMethod(...args) // because the last typed function call was someMethod(args...)

So, what I tried to said with 'delegate' inside noSuchMethod behaving specially, is in the conventional way of understanding functional programming, which is that given exactly the same inputs to a function, the function will throw exactly the same output. This does not hold true for object orienting programming though, where the outputs of a function depend on the context. In the examples above, the context of the execution of the functions noSuchMethod (the first time it was explicitly called, the second time it was not) was not the same. 'delegate' depends on the context, just like any function call inside an object can depend on the context. 'delegate' does not care about the current function that is being executed and its parameters, it depends on the context on which it was called. It just happens to be that the only way to call an object's function in a static typed language is by explicitly calling it, so anytime you arrive at a delegate statement inside a function it will repeat the exact same call. The exception to the rule is noSuchMethod, where you can arrive in two ways, directly or indirectly. 'delegate' behaves specially in this function because there can be different contexts that derive into it's execution.

The conventional way of understanding programming language statements and functions would lead us to interpret that what 'delegate' means inside noSuchMethod is the following:

return x.<invocation.memberName>(<invocation args>);

It should not be implemented like that, because if you do that, just like you said it, you would be doing reflection and you would lose static analysis (tree-shaking) capabilities.

@lrhn
Copy link
Member

lrhn commented Jun 26, 2019

So "the last static typed function call" would actually be the member invocation which caused the current member body to be run. An implicit invocation of noSuchMethod, caused by a program error, will not count towards this, it does not go on that stack. The delegate operation only works in instance members (like super or this references) because it depends on the name of the member being invoked.

We would have to retain a stack of those invocations. Maybe it can be derived from the actual run-time stack, but most likely it cannot because that would preclude leaving parameter variables on the stack and mutating them there. At least it would require copying mutated parameters to another location on the stack.
The delegate x operation would call another method with the exact same arguments. Inside any member other than noSuchMethod, this operation can be typed, because we know the type of invocation (at least if we assume that all optional parameters are present, which we should).
Inside noSuchMethod, we don't know the actual invocation that got us there, so the invocation will be dynamic.
That approach is still "safe" if you call noSuchMethod directly, the delegation will just call the noSuchMethod on the new receiver, which is always valid. In any case, it does not use the Invocation object as a source of what to invoke, so you can't introduce a call which didn't exist in the original source code. That is a good thing.
I'm not sure whether callableObject() would delegate as receiver() or receiver.call(), but either should work.It's still worse than the status quo because you can't (as easily) statically determine if a particular type's foo method is called. If the compiler sees that foo is only ever called on objects with a static type of Foo, and there is no subclass of Bar implementing Foo, then Bar.foo is definitely dead code.

I'm not sure whether callableObject() would delegate as receiver() or receiver.call(), but either should work.

Will delegate x; still return the result. That is, it is a tail call of the receiver, which means that you can reuse the arguments on the stack (if they are not mutated). If the delegate call is inside a catch or finally, then it might not be possible to implement it as an actual tail call.
Also, the delegate operation cannot be used inside nested functions because it must return from the instance member (just like yield can only be used inside an async* function, not inside nested functions).

Any method containing a delegate operation will know that. The implementation can choose to copy any mutable parameters, and all other functions should be unaffected, except noSuchMethod. That would mean that we won't need a stack on the side.
An invocation of noSuchMethod would have to take an extra hidden parameter describing the invocation causing it to happen. A direct invocation of noSuchMethod would have a hidden parameter describing the noSuchMethod invocation itself. We can detect noSuchMethod instance member invocations (or tear-offs) by name.

If you delegate to super, then noSuchMethod would have to work with that too:

class C {
  final C _default;
  C([this._default]);
  void foo() {}
  noSuchMethod(i) {
    if (_default != null) delegate _default;
    delegate super;
  }
}
class D extends C {
  D() : super(E());
  noSuchMethod(i) {
    print("DEBUG: ${i.memberName}");
    delegate super;
  }
}
class E implements C {
  int foo([String x]) { 
    print(x);
  }
}

main() {
  dynamic x = D();
  d.foo("yes"); // Prints "Symbol foo", then "yes".
}

Here the delegate from D.noSuchMethod to C.noSuchMethod would keep the top-of-stack invocation as well, so that C.noSuchMethod's delegate _default would call E.foo with "yes".

I'm convinced that this is possible and consistent. I'm not convinced the feature is worth the complexity :)
If noSuchMethod had been an operator, so it was only ever invoked by the system, then some of the complexity would go away.

If you do:

class C {
  noSuchMethod(i) { delegate this; }
}

I guess the outcome depends on whether it's a proper tail-call or not (stack overflow or infinite loop).

@icatalud
Copy link
Author

icatalud commented Jun 26, 2019

I'm convinced that this is possible and consistent. I'm not convinced the feature is worth the complexity :)

If I'm not wrong, you do believe it's worth implementing the proxy functionality, according to comments I saw on @natebosch post, but you are skeptical about the complexity of the 'delegate' functionality as the right approach to this feature. I think many people are looking forward for proxy functionality, and as I said it before, it is my believe that there is an important subset of people requiring mirrors just because of the proxy purposes. The question is now, what would be the best way to implement proxies? I think it would be important to take a decision on which path to take and add it to the backlog in case it is decided that it is worth including. If no decision is made, it will keep being a lost issue in the forums, the other post dates from November 2017. There are plenty of questions and requests for this feature that makes me believe it's definitely worth including (I'm looking forward for this, specially because I'm playing around with the flutter library). If the opinion is that other options are more suitable, then a different option should be taken. What is most relevant is to somehow include a proxy feature.

Some of the advantages of the delegate solution compared to the proxy solution proposed before are:

  • Strengthens Dart identity by creating a feature that only works because of Dart's current identity, particularly by overloading the unique noSuchMethod with more functionality that "escapes" the claws of static typing, making dart to be on the more flexible side of static typed languages
  • It's easier to deduce special behavior from a class (from a Dart knowledgeable perspective) when it comes from noSuchMethod definition because so far all special behavior is related to that method, being it natural to arrive there when a class does not behave as expected. Having a special var declaration as a proxy, would cause you took look at two things for special behavior, var declaration or noSuchMethod (this could be addressed for example by adding a proxy keyword in the class definition though)
  • It could allow to extend the current colon ( : ) notation to functions also (currently used only in constructors), further giving Dart a unique identity
  • It's a novel way of providing solution to a more complex feature like proxying as a "side effect" of what would seemingly be purely syntactic sugar
  • Gives syntactic sugar to a common function forwarding pattern
  • Adds one keyword that can be used for multiple purposes, being all the purposes consistent with the definition of the keyword behavior
  • It's unique (I might be wrong here, I'm not very knowledgeable of programming languages syntaxes)

Note that any kind of proxy benefits the language by addressing a subset of what the mirror functionality provides, allowing this subset of functionality to become achievable by static means. It should be a desired side effect to reduce the mirrors library utilization.

Disadvantages

  • It's prone to confusion because of the idea of a statement behavior inside a function depending on how the function was invoked. Although there is principle nothing wrong with that, it's not something people is used to in programming. This shouldn't be too hard to digest though, because the definition is consistent and because this "unexpected" behavior can only occur in noSuchMethod, being already a special function in Dart
  • It doesn't give syntactic sugar for a more general function forwarding pattern (it doesn't work for the same function signature with different name), but this could be revised and make some variation to allow it
  • More complex to include compared to a different proxy functionality?

Here the delegate from D.noSuchMethod to C.noSuchMethod would keep the top-of-stack invocation as well, so that C.noSuchMethod's delegate _default would call E.foo with "yes".

That's a beautiful example, it perfectly illustrates the behavior of delegate. One thing though, I would't limit delegate to return immediately, it's just like another function call, so after the delegate returns, the function keeps executing (you can do 'return delegate obj' of course). In your example the program should throw exception, because after calling 'delegate _default' there is a 'delegate super'. This is of course debatable, but I do not see why to limit the functionality. Being able to delegate multiple times might prove useful sometimes.

There are quite a few details that are worth discussing, like whether delegate should return or not, if it uses the original call args or the same args even if they were modified during the function execution (i would send the last version of the args, not the original) or if it can be used inside a non instance function.

It's very hard to progress an idea through posts in github, I suggest we polish the idea in a shared doc with a concrete proposal, including alternatives like a proxy, unless you are convinced that taking this road is the way to go. I will fill in something tomorrow, because I'm tired for today.

https://docs.google.com/document/d/1yr2GCgnNdwgrDDXPEb2Vm5rw1PCZIOHNM5lhwMlZMNk/edit?usp=sharing

@icatalud
Copy link
Author

@lrhn
I updated the doc, I hope can follow up on this. I'm assuming you work on the language?
Also I added you as a collaborator since I copy and pasted your examples. Let met know if you do not agree on that.

@lrhn lrhn added the request Requests to resolve a particular developer problem label Jul 1, 2019
@lrhn
Copy link
Member

lrhn commented Jul 1, 2019

The desired work-flow for language design is actually to keep it on Github, using an issue for discussion, and then perhaps a pull request with a markdown document for fine-tuning a strawman design.

I'm intrigues by the idea of delegating, but I'm also somewhat concerned about the potential run-time cost, and I think only being able to forward to a method with the same name is too restrictive.
Having a deeply nested expression affect the entire method is concerning (but then, so does await).

Technically this is forwarding, not delegating, so the delegate name might be confusing (doubly so to people from C# were "delegate" is just a closure).

@icatalud
Copy link
Author

icatalud commented Jul 2, 2019

I understand, I like markdown files. What I do not like like is that you cannot comment them. It's not interactive or cooperative, it discourages making comments to fix smaller things or giving hints on the best way to express an idea.

Nevertheless, like you said it, there is an "established" workflow that everyone should follow. I copy paste what's on the document.

Option 1: delegate (proxying as a side effect)

The keyword ‘delegate’ will be used to explain the approach. Alternative wording could be:
forward (to), repeat (on), recall (on), reapply (on), proxy

Introduces a new keyword ‘delegate’ to the language having the following “syntactic sugar” meaning:

”repeat the last typed function call that derived into the current function block on the supplied word.”

Note that the ‘supplied word’ implies it has to be a valid syntax, so it could only be a class instance var, ‘this’ or ‘super’. Example:

class Mixer extends StringMixer {
  B b;
  int mixNumbers(a,b,c) {
    return delegate b;  // equivalent to return b.mixNumbers(a,b,c)
  

  String mixStrings(a,b,c) {
    return delegate super;  // equivalent to return super.mixStrings(a,b,c)
  
}

Previous usage of ‘delegate’ is merely syntactic sugar, which could already prove to be useful as a shortcut of common forwarding patterns, being the most used super.methodName(args…).

This looks like a minor superficial feature, but by taking advantage of the definition given above and dart internals related with noSuchMethod, it’s possible to derive generalized proxying as a side effect. This approach therefore looks like a far more complex solution for proxying than a direct approach, reason being not the complexity of the functionality, but rather the fact that proxying needs to be deduced. The reasoning and consistency of this feature as a proxying solution requires a more detailed explanation.

Proxying: ‘delegate’ inside noSuchMethod

The definition of ‘delegate’ has a peculiar implication on it’s behavior inside noSuchMethod. Dart derives any call to nonexistent class methods into noSuchMethod. Therefore there are two ways of reaching the execution of noSuchMethod: directly and indirectly.
Directly means calling noSuchMethod like any method, instance.noSuchMethod(invocation). In that case ‘delegate’ has the same behavior as in any other function, ‘delegate x’ is syntactic sugar for x.noSuchMethod(invocation).
Indirectly is reaching noSuchMethod when Dart internally forwards a method call that is unexisting in the class to noSuchMethod. When noSuchMethod is reached indirectly, ‘delegate’ will invoke the original function that was attempted to be invoked, but that was derived into noSuchMethod. ‘delegate’ has therefore a dynamic behavior inside noSuchMethod, allowing to forward undefined methods to another object. This effectively provides the capability of easily creating proxy classes (classes that forward method calls to other objects). The following example illustrates ‘delegate’ behavior inside noSuchMethod taking into account most border cases:

class C {
  final C _default;
  C([this._default]);
  void foo() {}
  noSuchMethod(i) {
    if (_default != null) return delegate _default;
    delegate super;
  
}
class D extends C {
  D() : super(E());
  noSuchMethod(i) {
    print("DEBUG: ${i.memberName}");
    delegate super;
  
}
class E implements C {
  int foo([String x]) { 
    print(x);
  
}

main() {
  dynamic x = D();
  d.foo("yes"); // Prints "Symbol foo", then "yes".
}

Step by step what happens in the snippet:

  1. dynamic x = D();
  2. D() : super(E());
  3. C([this._default]); // an E instance comes as parameter to become _default, which is of type C, being an E instance a valid value because E implements C
  4. d.foo("yes"); // d implements foo(x) ? no, therefore dart forwards call to D.noSuchMethod
  5. D.noSuchMethod(i) // indirectly called noSuchMethod on d from a d.foo call
  6. calls print("DEBUG: ${i.memberName}"); // prints “Debug: Symbol foo”
  7. delegate super; // repeat the last function call that derived into current block in super, so it’s equivalent to super.foo(x)
  8. C has no such foo(x) method definition (it has foo, but receiving no parameters) therefore dart invokes noSuchMethod on C
  9. C.noSuchMethod(i) // indirectly called noSuchMethod on d.super from a super.foo call
  10. if (_default != null) return delegate _default; // _default is and E instance and because noSuchMethod was invoked from a foo(x) call, delegate _default is equivalent to _default.foo(x)
  11. E.foo(x) // E defines foo(x), so it’s called in _default
  12. print(x); // prints “yes”
  13. Ends program

Optimization with colon ( : ) notation

The following notation should be allowed to avoid entering a function block that only delegates to another object:

class Mixer extends StringMixer {
  B b; C c;
  int mixNumbers(a,b,c): delegate b;
  String mixStrings(a,b,c): delegate super;
  noSuchMethod(i): delegate b, super, c;
}

That way the compiler understands that any call to Mixer.mixNumbers is equivalent to b.MixNumbers.
For noSuchMethod, delegate will use the first available interface that implements the method in order: b, super and c.

Should ‘delegate’:

  • return immediately? No, it allows more use cases to not force return
    be called with the original supplied args? No, it should be called with the current value of the args. In the case of noSuchMethod args are not accessible though (unmodifiable).
  • be usable within a non class method? Yes, it would make the same function call in the supplied object
  • be usable within an anonymous function? No, they have no name, so what method would you be calling in the supplied instance?

Alternative additional features

In order to also address forwarding function calls to any method and not just class instances that have the exact same method declaration (including method name), perhaps it could also be possible to do ‘delegate(function);’. Is that worth it?

Option 2: proxy var

A proxy var inclusion is a more straightforward and explicit implementation, therefore it’s easier to understand and apply. Following example is the implementation:

class TreeHouse implements House {
  final proxy House _house;
  final Tree tree;
  TreeHouse(House house, Tree tree) : this._house = house, this.tree = tree;
   int get height => _tree.height + _house.height;  
   // all members of House are forwarded to _house.
}

Another alternative could be:

class P implements A, B, C, D {
  A a; B b; C c; D d;
  proxy: c, a, d, b  // this exposes uses the first interface available according to the order of the objects
  // some method overrides ...
}

Can proxies be used inside functions blocks? That could prove useful to forward the functionality to multiple objects, creating some kind of "concentrator" classes, easily extending the functionality of one class into a "arrays" (with Option 1 would be possible to do this).

@rrousselGit
Copy link

rrousselGit commented Feb 27, 2020

Is there any known workaround to decorating/composing any functions without losing type-safety?

Right now it seems that we either have to restrict our decorator to a specific number of parameters, or we have to use on dirty noSuchMethod+Function.apply+dynamic tricks.

@lrhn
Copy link
Member

lrhn commented Feb 28, 2020

It's not possible to abstract over parameter list structure, so no.

If you want something to work for functions with different signatures, you have to implement it for every signature that you need to support, or use Function or Function.apply in some way.

@rrousselGit
Copy link

In the end, I think there are three main features:

  • adding new parameters to a function
  • removing/injecting parameters to a function
  • including/excluding functions with a specific prototype

For example, Flutter's StatelessWidget could be represented with a function that injects a BuildContext & Key, where we define

final example = decorator(
  (BuildContext context, {required int count}) {
    return Text(
      '$count',
      style: Theme.of(context).textTheme.body1,
    );
  },
); 

This:

  • adds a new Key key parameter to the resulting function
  • removes/inject the context parameter
  • excludes functions that receive a parameter named "key" (as it would conflict with the added Key key)

The resulting prototype of example after the composition would then be:

Widget example({Key key, required int example});

I wonder if we could solve this by adding syntax for partial prototypes, which we could then use with generics and return values.

Like:

(BuildContext context, { Key key, ... })

This defines a generic prototype for a function that always have one context positional parameter, one optional Key parameter, and potentially any named parameters.

So we could implement the previously mentioned StatelessWidget decorator with:

// A function that returns a widget, receives a positional BuildContext, does **not** have a "key" named parameter,
// and may have other named parameters
typedef InputFunctionalWidget<T extends { ^key }> = Widget Function(BuildContext context, { ...T });

typedef OutputFunctionalWidget<T extends { ^key }> = Widget Function({Key key, ...T});

OutputFunctionalWidget<Named> statelessWidget<Named extends { ^key }>(InputFunctionalWidget<Named> decorated) {
  return ({Key key, ...Named namedParameters) {
    return Builder(
      key: key,
      builder: (context) => decorated(context, ...namedParameters),
    );
  };
}

We can then do:

final example = statelessWidget((context,  { required int count }) {
  return Text(
    '$count',
    style: Theme.of(context).textTheme.body1,
  );
}

Another example would be a debounce decorator:

void Function(...Positional, {...Named}) debounce<Positional extends [], Named extends {}>(
  Duration duration,
  void Function(...Positional, {...Named}) cb,
) {
  Timer timer;

  return ([...Positional positional], {...Named named}) {
    timer?.cancel();
    timer = Timer(duration, (_) => cb(...positional, ...named);
  };
}

Which we can then use as:

final example = debounce(Duration(seconds: 1), (int count, {String name}) {
  print('$count $name');
});

void main() {
  example(0, name: 'John');
  example(42, name: 'Mona');

  // after 1 second, prints "42 Mona" but does not print "0 John"
}

Then we can think of some syntax sugar so that we don't lose all the function names in the stack trace

The @Decorator sounds good to me:

@debounce(Duration(seconds: 1))
void example(int count, {String name}) {}

@statelessWidget()
Widget example(BuildContext context, { required int count }) {}

@Levi-Lesches
Copy link

Levi-Lesches commented Jul 14, 2021

class TreeHouse implements House {
  final proxy House _house;
  final Tree tree;
  TreeHouse(House house, Tree tree) : this._house = house, this.tree = tree;
   int get height => _tree.height + _house.height;  
   // all members of House are forwarded to _house.
}

@icatalud, What would be the difference between that and the following?

class TreeHouse extends House {
  final Tree tree;
  TreeHouse(/* params for House */, this.tree) : super(/* params for House */);
  int get height => tree.height + super.height;
  // inherits all members of House, no "forwarding" required
}

It's not that different since you need to know the params for House whether you're passing args to super or creating a House object to pass as a proxy.

Copying from @lrhn:

Forwarding an entire interface: Instead of relying on noSuchMethod to call another class, perhaps we could designate specific getters as proxies for their interface. Something like proxy House house; would mean that all members of House which are not overridden by the current class declaration, are introduced and forwarded to the same member on house.

That feels like regular extends, isn't it? Instead of proxy and _house, you have this and super. Maybe I'm missing something, but I don't see the difference.

EDIT: To borrow from #493 (comment), we can allow the word super instead of this in initializer lists, to reduce the boilerplate needed to call the super constructor for House:

class TreeHouse extends House {
  final Tree tree;
  TreeHouse(super.barkType, super.height, super.leafColor);  // equivalent to passing these to super()
  int get height => tree.height + super.height;
  // inherits all members of House, no "forwarding" required
}

@lrhn
Copy link
Member

lrhn commented Jul 26, 2021

The difference between extends and forwarding is that:

  • extends requires a generative constructor and creating the superclass instance at the same time as the subclass instance, where forwarding allows you to use an existing instance of the forwarded interface, created in whatever way you need it.
  • extends allows overriding methods, forwarding does not (that's where "forwarding" differs from "delegation", the latter does change the this reference of the methods you delegate to).
  • You can only extend one class, you can forward to multiple interfaces.
  • "Favor composition over inheritance".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

5 participants