-
Notifications
You must be signed in to change notification settings - Fork 205
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
Comments
There are several requests in this issue (explict or implicit).
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 Forwarding to another method: 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 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 Forwarding |
Some discussion of a similar idea I head: #3748 |
@Updated
@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?
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 Proxying with noSuchMethod
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). 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:
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:
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() 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).
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.
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. |
About
That is not why I don't want it. We have dynamic invocations and The reason I don't want it is that it makes it harder to analyze the program. If you have an If you don't have such a reflective invocation, and there is no invocation of a method named 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 I'm opposed to adding reflective features outside of This brings us back to Using return x.<invocation.memberName>(<invocation args>); can definitely be made to work. It allows me to declare an 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 If you couldn't invoke I know it's convenient, it allows mocks and decorators to be simple and general, |
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:
(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:
That would happen according to the definition of 'delegate' provided above. On the other hand, if you do the following:
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:
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. |
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 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. I'm not sure whether Will Any method containing a If you delegate to 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 I'm convinced that this is possible and consistent. I'm not convinced the feature is worth the complexity :) 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). |
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:
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
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 |
@lrhn |
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. Technically this is forwarding, not delegating, so the |
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: 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 noSuchMethodThe 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. 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:
Optimization with colon ( : ) notationThe 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. Should ‘delegate’:
Alternative additional featuresIn 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 varA 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). |
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 |
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 |
In the end, I think there are three main features:
For example, Flutter's final example = decorator(
(BuildContext context, {required int count}) {
return Text(
'$count',
style: Theme.of(context).textTheme.body1,
);
},
); This:
The resulting prototype of 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 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 @debounce(Duration(seconds: 1))
void example(int count, {String name}) {}
@statelessWidget()
Widget example(BuildContext context, { required int count }) {} |
@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 Copying from @lrhn:
That feels like regular EDIT: To borrow from #493 (comment), we can allow the word 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
} |
The difference between
|
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:
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?)
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:
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:
The text was updated successfully, but these errors were encountered: