-
Notifications
You must be signed in to change notification settings - Fork 207
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
Forwarding functions #3444
Comments
I would like to suggest the bool compare(String a, String b) = super.compare; syntax, which is shown twice in #3427, instead of bool compare(String a, String b) ==> super.compare; Kotlin's single-expression functions (equivalent to Dart's arrow functions) use an equal sign: fun compare(a: String, b: String): Boolean = a.length < b.length I have always found |
@Wdestroier, I did consider using In any case, by all means let's look at it, perhaps it grows on us: Object f1([int]) = g1; // Or `= A.g2`. We could do this.
bool g1([num n = 3.14]) => n > 0;
class A {
static Object f2([int]) = g2; // Or `= g1;`. We could do this, too.
static bool g2([num n = 3.14]) => n > 0;
void foo() {
void g3([num n = 3.14]) => print(n > 0);
void Function(int) f3 = someCondition ? (int) = g1 : (int) = g3; // Confusing to read?
[1, 2, 3].forEach(f3);
}
}
class B {
bool g4([num n = 3.14]) => n > 0;
}
class C implements B {
final String s;
C(this.s);
bool g4([num? n = 5.25]) => n > 10;
Object superG4(int) = super.g4; // We can do this, too.
superG4 = super.g4; // Confusing to read?
// Forwarding to a different object.
mySubstring = s.substring; // Confusing?
substring = s; // Confusing again?
}
void main() {
B b = C();
[0, 10, 100].forEach((int) = b.g4); // Possible, but perhaps confusing?
} |
Thank you, I agree. |
I really like this feature; however, the following feels off to me (maybe I am not used to it):
If possible I would write them as: void Function(int) f3 = someCondition ? g1 : g3;
// or using ==>
void Function(int) f3 ==> someCondition ? g1 : g3;
// and
[0, 10, 100].forEach(b.g4); @eernstg Are there cases where we explicitily need to write a forwarding function in the form |
Sure, that would be sufficient, and we would always have the assignability as long as the forwarding function is required to have a type which is a supertype of the forwardee. The main use case would probably be that you want to enforce that supertype: If const _secret = MyType();
void _g1({MyType myType = _secret}) {...}
void _g2(int arg1, [int arg2 = 42]) {...}
void foo(void Function(Function) callback) {
...
// They can call `_g1`, but they can't use the default value.
callback({required MyType myType}) ==> _g1);
...
// They can call `_g2`, but we're guaranteed that these calls
// will use the default value `42` as the second argument.
callback((int arg1) ==> _g2);
...
} Being lazy, I just used the type So the forwarding function literal might not be a very common construct, but it still seems potentially useful to me. |
Somewhat related to #4159. |
Forwarding is just a degenerate case of a more general problem of creating wrapper functions. The difference is that the wrapper can add or remove some parameters to/from the list of original parameters, and can call the target explicitly in its body. Here's a raw idea for discussion. Let typedef G=(g.*); // record type
void f(g.*) => g(...); // 3 dots mean: take parameters from the context
void f1(g.*) {
print('HELLO');
g(...); // same meaning as above
print('BYE');
} To override parameters in the parameter list, we must use a special syntax, e.g. void f({int x! =42}, g.*) => g(...); Here, x overrides the parameter x coming from g, with a different default value. void f(int zzz, g.*) {
// code using zzz
g(...);
} To override parameters while forwarding, use syntax g(y!:15, ...); // pass a different value of y One thing I haven't figured out yet is how to remove parameters from g.*. Maybe something like void f(g.* hide x) {
g(x!:0, ...);
} |
Interesting! I thought about similar things, and it would indeed be tempting to enhance the mechanism such that it is possible to modify actual argument list in the "near-forwarding" invocation. The catch is that we might need to have a huge amount of mechanism in order to allow for enough flexibility to handle the cases that arise in practice. Simple mechanisms might look good in examples, but there's going to be a steep cliff to jump off it it only works in the cases that we've considered when this mechanism was designed, and it may or may not work for real cases. |
I feel like straight forwarding is best done through the existing forwarding syntax present in factories; Especially since it should be relatively trivial to implement given the existing factory code. That's one thing. Otherwise is getting the argument list itself in record form, and pass it straight in, where it automatically spreads. Ie, single-argument functions that contain a record are equivalent to an argument list with those fields Ie: Then, we just use record type aliases to propagate them. we would then get: typedef Args = (String key, {bool? flag});
void func(Args args) => Somewhere.realFunc(args);
// Shorthand. Could be useful for other things
void func(Args args) = Somewhere.realFunc;
func('something', flag: true); Then, if we want to exclude or add parameters, we would need static record type operations, which can be extremely useful in other places. |
More examples: typedef A=({int a, String b});
typedef B=(...A.*);
typedef B=(...A.(* hide b));
typedef B=(int c, ...A.(* hide b));
typedef B=(...A.(* show b));
func(...A.*) {/*body*/}
func(...A.(* hide a)) {/*body*/}
// etc.,
class C {
final int a;
String b;
C(...this.*);
C.n1(...this.(* show a, b));
C.n2(...C.new.(* show a, b)): this(...);
}
class D extends C {
double d;
// etc.
D(...this.*, ...super.new.*): super(...);
D.id(...this.*, ...C.new.*): super(...);
} |
@TekExplorer wrote, about plain forwarding:
I don't think it will be a huge implementation effort, but those two things are actually quite different. Redirecting factory constructors are resolved during compilation, so that's basically an inlining mechanism. A forwarding function invocation needs a bit of magic in order to preserve the semantics of default values of parameters: abstract class A {
int foo([int i]);
}
class B1 extends A {
int foo([int i = 1]) => i;
}
class B2 extends A {
int foo([int i = 2]) => i;
}
class C {
A a;
C(this.a);
foo ==> a;
}
void main() {
var c1 = C(B1()), c2 = C(B2());
print('Should be 1: ${c1.foo()}, should be 2: ${c2.foo()}');
} The approach to forwarding that one might take at first would be to say that However, there is no way to choose a value for So we can't write a true forwarding function in Dart when the forwardee has one or more parameter default values, we need to have some extra mechanism. This mechanism could be support for detecting whether or not a given optional parameter has actually been passed (independently of the default value, that is, we can't just check whether it's null). We had such a mechanism many years ago, and it was removed for a whole bunch of reasons, so it isn't likely to be re-added. However, let's just consider how it could be used. We'll use the syntax class C {
A a;
C(this.a);
int foo([int i = k]) => passed(i) ? a.foo(i) : a.foo();
} However, this won't scale up very well if we're dealing with a function that accepts, say, 10 optional named parameters, because it will have to spell out all 1024 possible invocations of the forwardee. We aren't going to have to write that huge expression with 1024 different invocations manually, it will be generated by the compiler, but it may still cause the program size to blow up. So we'd actually want a different kind of "magic". One possibility is that the runtime introduces a new "universal object" which can be passed everywhere (it's like an instance of This approach would only have a linear cost (so 10 optional named parameters would be handled in 10 steps, not 1024 steps). In any case, this illustrates why forwarding in Dart isn't trivial. ;-)
That's an interesting idea, and the language team has already had discussions about possible features whereby actual argument lists to functions could be considered equivalent to record values. However, it's a non-starter to require that all function calls will receive their actual arguments as a record, and all the existing call sites will implicitly create such a record and pass it. That is simply prohibitively expensive in terms of run-time performance. The reason why this perspective is relevant is that we can't predict which functions/methods will be the target of a forwarding invocation, which could mean that all functions and methods must work in such a way that a forwarding invocation is possible. We can't allow significant slowdowns of such a basic thing as function/method invocations in order to support a new feature, no matter how nice that feature might be. Another reason why it may be expensive to pass an actual argument list as a record value is that record values don't have optional parameters (positional or named). This means that a function that accepts 10 optional named parameters will have to be prepared to accept 1024 different types of record, representing each of the possible combinations of passed/non-passed parameters. It might have to perform 1024 type tests on the actual argument record before it finds the one that has the right combination of present/absent arguments, and it would then need to assign the default values to the remaining parameters. That doesn't scale well, either. So this argument-list-is-record idea is worth exploring further, but it wouldn't go anywhere if it degrades the run-time performance of all programs. |
@tatumizer wrote:
Interesting! It looks like you're exploring a mechanism that would allow us to abstract over signatures or at least over formal parameter lists (so we wouldn't have to duplicate a list of 123 named parameters with all their associated information, we could just refer to that list as a declared entity). This could clearly be more expressive. The mechanism I've proposed here allows for the formal parameter list to be (1) written explicitly (no abstraction, full expressive power available, it just has to be a parameter-wise subtype such that the forwarding call is guaranteed to be type correct). It also allows the formal parameter list to be (2) omitted entirely, in which case we use the formal parameter list from the forwardee (this may include private types from other libraries, so it's not just a textual copy of the formal parameter list). Similarly for the type parameter list. It's worth keeping in mind that a forwarding (or a more general wrapping) mechanism could be made more flexible and powerful by means of this kind of abstraction. As usual, we'd need to find a good balance between the amount of machinery we're introducing and the amount of benefit that developers will gain from this machinery in practice. |
The main reason was that it didn't prevent the exponential blowup for multiple optional named parameters with default values. The feature itself is not the problem, the problem is not being able to forward a lack of argument using only a linear amount of code. Argument elements would solve that. void foo({int v1, int v2, ... int vn}) =>
bar(v1: if (passed(v1)) v1, …
vn: if (passed(vn)) vn); such that no I'd be fine with having both operations. It's having only one that causes a problem. |
@eernstg wrote:
The new syntax is localized to the places where we have to either declare or to pass a list of parameters, so hopefully you won't need too much machinery (not sure). On the plus side, the device turns the list of parameters into a kind of "mathematical object" with a small number of operations defined on it. There's something satisfying in having such a concept in a language - the users will appreciate it. |
You seem to forget that redurecting factories can't have default values because the real function defines them That syntax is also... Kinda awful. At that point just do It doesn't even look like a method, and the arrow syntax is being misused here I think. Of course, we can't do that with methods, which is why we need proper redirection at all, otherwise I would just stick to the record type as Args issue, plus the record type operations issue for flexibility. |
I also don't see any reason why argument records would be any more expensive than what we have now, since it's not like we can do anything (directly) with records anyway. Plus, what does the argument list look like in memory in the first place? Could it just... Be a record? Especially since records were made to look like argument lists. We're just missing default and optional values, which I'm sure we can come up with a solution for. |
I really don't like the idea of a I would much rather it be some kind of coercion - as a straw man: Of course, with that literal example you would need to wrap the value in a Some, but perhaps with implicit constructors or some kind of coercion, it can sort of go away. It honestly kind of sucks that we couldn't have used that for null from the get go, and get associated useful language features to make it great the way we do now with null, but I understand that that would have been way more complicated of a transition. I do like the conditional passing as an independent feature for the sake of preserving default values when desired. |
I'd rather have a pseudo-method
Programming without the value NOTE: with the proposed syntax of redirection like |
Most likely it's a sequence of slots on the machine stack. Maybe including a count of arguments and a mapping from names to positions (but optimized calls can perhaps resolve some names statically. Dynamic invocations are the ones that really need to have all the structure provided at runtime.
That is one option that I'm pretty sure I've suggested somewhere. Allow parameters to be There are many ways to solve this issue. I like the ones that do more than just solve one issue. |
Incorrect. It would have been very possible to exist without null - languages like Rust do just fine without it. The Option monad is pretty neat. Of course, since dart does not have such a monad built in with special treatment (and something tells me thats not likely to change) we would then need a more general language feature that just so happens to suite our purposes. The reason such a monad would be useful, is because unlike nullable types, Option<> is just a generic, so it can be nested as desired, unlike nullables which cannot. It's easy to do |
@lrhn: the syntax of bulk-declaration of parameters makes it easy to incorporate a class B extends A {
B(late A.new.*): super(...);
} This means: copy all parameters of The same is true when you want to forward explicitly:
In the rare cases where you really want to know whether the parameter was passed or not (remember, forwarding is already covered, so you need a different reason for wanting this), you can certainly devise a method like I've also realized that the syntax I proposed for bulk declaration was too noisy: there's no need to write // rather then writing this
g(...f.*) => f(...);
// write this:
g(late f.*) => f(...); |
Hm... Using late to determine if something has been passed in is an interesting possibility. I still don't like that it feels like reflection, but it's better than a |
See the discussion of late parameters in #3680 |
@TekExplorer wrote:
Here is the example again: abstract class A {
int foo([int i]);
}
class B1 extends A {
int foo([int i = 1]) => i;
}
class B2 extends A {
int foo([int i = 2]) => i;
}
class C {
A a;
C(this.a);
foo ==> a;
}
void main() {
var c1 = C(B1()), c2 = C(B2());
print('Should be 1: ${c1.foo()}, should be 2: ${c2.foo()}');
} It's correct that for a redirecting factory there is no support for specifying default values; the reason for this is that redirecting factories were designed to be eliminated at compile time. The point I was making was that redirecting factories are very different from forwarding methods, and this is just one of the reasons why that is true. (As I mentioned in the initial posting, a tear-off of a redirecting factory constructor needs forwarding semantics because we can't just use a tear-off of the redirectee, but that's exactly one of the motivations for proposing this mechanism, because we don't have a solution for that, yet.) Another reason why forwarding methods require more than redirecting factories is that all the regular object-oriented mechanisms must work with forwarding methods. For example, late binding must work, which means that an instance of In my example, the forwarding method was declared as The proposed mechanism allows for declarations like
Note that
I'm not quite sure what this means, perhaps you're making the same point? |
@TekExplorer wrote:
const count = 100_000_000;
int f1(int x, int y, int z) => x + y + z;
int f2((int x, int y, int z) r) => r.$1 + r.$2 + r.$3;
int sink = 0; // The compiler can't eliminate assignments to this variable.
void main() {
Stopwatch stopwatch;
double elapsedTime;
stopwatch = Stopwatch()..start();
for (int i = 0; i < count; ++i) {
sink = f1(i, i, i);
}
elapsedTime = stopwatch.elapsedMilliseconds / 1000;
print("${elapsedTime.toStringAsFixed(2)} seconds"); // '0.27 seconds'.
stopwatch = Stopwatch()..start();
for (int i = 0; i < count; ++i) {
sink = f2((i, i, i));
}
elapsedTime = stopwatch.elapsedMilliseconds / 1000;
print("${elapsedTime.toStringAsFixed(2)} seconds"); // '1.81 seconds'
} (Caveat emptor, this is a micro-benchmark.) As @lrhn mentioned, actual arguments of a function invocation (including instance method invocations) can very likely be stored on the run-time stack. This could be as references to existing heap objects or immediate representations like small integers (on some platforms), but it does not introduce a new object. Another option is to pass actual arguments in registers. A record needs to be a proper object in the heap. It actually also needs to be an instance of a generic class, because it must be possible to assign It might be possible to reduce the overhead associated with actual-argument-lists-are-records using all kinds of optimizations, but it is a very tall order. It's basically a non-starter to change Dart such that every actual argument list is a record. Another thing that we've discussed in the language team and which is doable is to support a mechanism whereby a record is subject to "spreading" into an actual argument list. For example, |
@TekExplorer wrote:
+1! |
@tatumizer wrote:
This might be similar to #2269, except that #2269 would need to be generalized in order to be able to handle the case where the default values of the forwardee aren't known statically. |
One request which comes up repeatedly in this discussion is "introduce language mechanisms to abstract over formal parameter declaration lists and actual argument lists", e.g., this comment. This would allow us to declare many formal parameters concisely, and passing them one in a forwarding call, with various degrees of modification (depending on how fancy those language mechanisms are). I already had the following in the original posting, and I certainly acknowledge that this could be generalized: One forwarding method: 56 lines of code in current Dart, reduced to 1 lineextension on BuildContext {
TextStyle textStyleWith({
bool? inherit,
required Color color,
Color? backgroundColor,
double? fontSize,
FontWeight? fontWeight,
FontStyle? fontStyle,
double? letterSpacing,
double? wordSpacing,
TextBaseline? textBaseline,
double? height,
TextLeadingDistribution? leadingDistribution,
Locale? locale,
Paint? foreground,
Paint? background,
List<Shadow>? shadows,
List<FontFeature>? fontFeatures,
List<FontVariation>? fontVariations,
TextDecoration? decoration,
Color? decorationColor,
TextDecorationStyle? decorationStyle,
double? decorationThickness,
String? debugLabel,
String? fontFamily,
List<String>? fontFamilyFallback,
String? package,
TextOverflow? overflow,
}) => CupertinoTheme.of(this).textTheme.textStyle.copyWith(
inherit: inherit,
color: color, // Declared as `Color? color`.
backgroundColor: backgroundColor,
fontSize: fontSize,
fontWeight: fontWeight,
fontStyle: fontStyle,
letterSpacing: letterSpacing,
wordSpacing: wordSpacing,
textBaseline: textBaseline,
height: height,
leadingDistribution: leadingDistribution,
locale: locale,
foreground: foreground,
background: background,
shadows: shadows,
fontFeatures: fontFeatures,
fontVariations: fontVariations,
decoration: decoration,
decorationColor: decorationColor,
decorationStyle: decorationStyle,
decorationThickness: decorationThickness,
debugLabel: debugLabel,
fontFamily: fontFamily,
fontFamilyFallback: fontFamilyFallback,
package: package,
overflow: overflow,
);
}
// Becomes
extension on BuildContext {
textStyleWith({required Color color, ...}) ==> CupertinoTheme.of(this).textTheme.textStyle.copyWith;
} Note that the return type in the abbreviated form is still The only kind of abstraction I suggested was (1) that That said, I'd still like to have support for the straightforward and faithful forwarding semantics using a concise notation like Next, there's a request to "introduce language mechanisms to abstract over passing or not passing an actual argument", e.g., this comment and this issue. As I mentioned earlier, I certainly appreciate the value of this feature. I just don't think it's sufficiently powerful to fill the whole bill when it comes to method forwarding. Not passing an actual argument will indeed yield a faithful reproduction of the semantics of the direct call with respect to that parameter and its default value. However, I think it needs to be combined with the previous kind of feature (allowing us to abstract over formal parameter declaration lists and actual argument lists). Otherwise we'll get an even more verbose result: extension on BuildContext {
TextStyle textStyleWith({
bool? inherit,
required Color color,
Color? backgroundColor,
double? fontSize,
FontWeight? fontWeight,
FontStyle? fontStyle,
double? letterSpacing,
double? wordSpacing,
TextBaseline? textBaseline,
double? height,
TextLeadingDistribution? leadingDistribution,
Locale? locale,
Paint? foreground,
Paint? background,
List<Shadow>? shadows,
List<FontFeature>? fontFeatures,
List<FontVariation>? fontVariations,
TextDecoration? decoration,
Color? decorationColor,
TextDecorationStyle? decorationStyle,
double? decorationThickness,
String? debugLabel,
String? fontFamily,
List<String>? fontFamilyFallback,
String? package,
TextOverflow? overflow,
}) => CupertinoTheme.of(this).textTheme.textStyle.copyWith(
inherit: if (??inherit) inherit,
color: color,
backgroundColor: if (??backgroundColor) backgroundColor,
fontSize: if (??fontSize) fontSize,
fontWeight: if (??fontWeight) fontWeight,
fontStyle: if (??fontStyle) fontStyle,
letterSpacing: if (??letterSpacing) letterSpacing,
wordSpacing: if (??wordSpacing) wordSpacing,
textBaseline: if (??textBaseline) textBaseline,
height: if (??height) height,
leadingDistribution: if (??leadingDistribution) leadingDistribution,
locale: if (??locale) locale,
foreground: if (??foreground) foreground,
background: if (??background) background,
shadows: if (??shadows) shadows,
fontFeatures: if (??fontFeatures) fontFeatures,
fontVariations: if (??fontVariations) fontVariations,
decoration: if (??decoration) decoration,
decorationColor: if (??decorationColor) decorationColor,
decorationStyle: if (??decorationColor) decorationStyle,
decorationThickness: if (??decorationThickness) decorationThickness,
debugLabel: if (??debugLabel) debugLabel,
fontFamily: if (??fontFamily) fontFamily,
fontFamilyFallback: if (??fontFamilyFallback) fontFamilyFallback,
package: if (??package) package,
overflow: if (??overflow) overflow,
);
}
// Versus
extension on BuildContext {
textStyleWith({required Color color, ...}) ==> CupertinoTheme.of(this).textTheme.textStyle.copyWith;
} By the way, did you notice the bug at We could certainly aim for a proposal that has all of these elements (the ability to faithfully handle default values plus the ability to quantify over formal parameters / actual arguments), and it would be quite powerful. I also think it would be nice to be able to test whether a formal parameter has been bound, and whether a late variable with no initializing expression has been initialized. However, I still think that it may be useful to have the specialized forwarding syntax, even if it is considered to be syntactic sugar for code that uses class InterceptClearList<X> implements List<X> {
final List<X> _list;
void Function<Y>(List<Y> self)? callback;
InterceptClearList(this._list);
void clear() {
callback?.call(_list); // Call if not null.
_list.clear();
}
* ==> _list; // Forward all other members to `_list`.
}
void foo(List<int> xs) {
xs.clear(); // Haha! This is where it happens.
}
void main() {
var xs = [1, 2, 3];
xs = InterceptClearList(xs)..callback = <Y>(ys) {
print('Warning: Someone is clearing a List<$Y>!');
};
...
foo(xs); // 'Warning: ...'.
...
} |
@eernstg: I think "delegation" is a more established name for the concept. "Forwarding" may be considered a more general concept with fine-grained control over the parameter lists. |
I think I'll keep using the word 'forwarding' here. Lieberman wrote this paper, ACM DL, in 1986 where 'delegation' was defined to be a mechanism that invokes a method on a different object than the receiver, but preserving the notion of Lieberman's work has been very influential in the topic area of delegation, to some extent because he pointed out the need to distinguish between forwarding and delegation at a time where this wasn't generally acknowledged, so I think it's fair to say that this is the standard terminology on those matters. It's certainly the terminology that I'm using here. Delegation is a more powerful mechanism than forwarding, but also a mechanism which is very tricky to handle in terms of safe, static typing. As an example, this paper is all about handling delegation in a type-safe manner, and it shows how difficult this is. In contrast, a language like Self uses delegation as one of the very few (but powerful) mechanisms in the language, and this enables all kinds of weird and wonderful things that have inheritance as a special case. Self is extremely dynamic. Here is an example that shows the difference (assuming that we can delegate to an object by declaring class A {
void foo() => bar();
void bar() => print('A.bar');
}
class Delegator {
delegate A(); // Delegates `foo` to an `A`.
void bar() => print('Delegator.bar');
}
class Forwarder {
void bar() => print('Forwarder.bar');
* ==> A(); // Forwards 'foo' to an `A`.
}
void main() {
var d = Delegator();
var f = Forwarder();
d.bar(); // 'Delegator.bar'.
d.foo(); // 'Delegator.bar'.
f.bar(); // 'Forwarder.bar'.
f.foo(); // 'A.foo'.
} I'm proposing to add support for forwarding in this issue, not delegation.
The abstraction which is provided by method1 ==> obj;
method2 ==> obj; One generalization could be to support multiple methods, perhaps specified as a comma separated list of names: method1, method2 ==> obj; This would still be compatible with situations where we want to specify some elements of the signature: MoreGeneralReturnType method1, method2(MoreSpecialParameterType p) ==> obj; This could be useful if
Interesting! Iterable.(* hide length) ==> _list; This would forward all other members if
I should mention that a 'delegate' in C# is just a function type, it has no particular relevance to the notion of forwarding in this context. However, it does have a very special property which is the ability to hold more than one function pointer, in a specific order. I don't really think we'd want to introduce anything of this nature into Dart. In particular, a multicast delegate in C# will ignore all results if it has return type void, and it will return the result from the last function invoked if the return type is non-void (and ignore all the other results). This seems very ad-hoc to me. So I'd prefer that Dart developers write a single function/method which has the desired combined semantics. |
I feel like we should try to stick to established patterns somehow. Like, we have super. and this. In initializer lists. Could we find a good way to do something similar for forwarding? Like: class A {
final B b;
void action(it.n1, it.n2, {Specific it.n3, required it.n4}) = b.action;
void action(.n1, .n2, {Specific .n3, required .n4}) = b.action;
void action(n1, n2, {Specific n3, required n4}) = b.action;
// Blind forwarding (implied type)
void action(*args) = b.action;
// _ for context type inference, if we think the above is dynamic
void action(_ *args) = b.action;
// More explicit. Could be a typedef
void action((int n1, String n2, {NonSpecific n3, Foo n4}) *args) = b.action;
// Other possibilities may be present.
} Possibly find either a word to use The forwarding syntax is reused, and intuitive. I think that if we really want to do more than that, a macro is probably better suited, because having those members all present on the class is probably better, and much less magic. We also can't do mass forwarding as-is safely anyway due to the forwarded changing itself suddenly becoming a breaking change, even when it shouldn't be. That's a lot of implicit stuff to be comfortable with. |
@eernstg: other languages have no qualms reusing the verb "delegate" how they see fit, with no regard to the cited paper. E.g. in rust, "delegate" means (exactly? more or less?) the same as whatever you mean by "forward" https://crates.io/crates/delegate If you don't like "delegate" and prefer to call it "forward" then you can write so: |
@TekExplorer wrote:
I think it will be useful to be able to write a faithfully forwarding invocation in terms of lower-level mechanisms. I also think it will be useful to have support for abstraction (such that we don't get the verbose and error prone form which is obtained by building the invocation from first principles using a lower-level mechanism). Finally, I think it will be very convenient to be able to obtain the signature of the forwarding method implicitly from the forwardee (that is, we get the return type from there if it isn't specified explicitly, and we get the formal parameter declarations that weren't specified explicitly).
These notations are concerned with the third task: Writing an abbreviated signature and getting the missing parts from the static type of the forwardee. For the declarations shown above, some parameters don't have a type (e.g., I think this kind of abstraction is already covered in a very similar way by my proposal: I'd write I don't see a need to use special syntax for the explicitly declared parts of the signature, it works just fine to declare the formal parameters using the standard syntax of formal parameters. We could consider supporting reorganizations of the parameter list (e.g., changing the order of some positional parameters, changing some named parameters to positional or vice versa, and so on), in which case we would need to mention the names of positional parameters. However, I'd prefer to omit this kind of feature: If you want to change the shape/ordering of the formal parameter list then you must write it out in full. A major reason why this is a dangerous feature is that it makes the names of positional parameters in the forwardee significant (e.g., renaming one of them would now be a breaking change). With respect to
For the second task, I think we'd want something like
I don't think this is a given. Mass forwarding is likely to be connected with a typing relation: class A {
...
int get aThing => 24;
void anotherAThing(num n) {}
}
class B implements A {
final A a;
B(this.a);
void bThing() {} // New, not in `A`'s interface.
int get aThing => 42; // `B` will have its own implementation of `aThing`.
* ==> a; // Forward all other members to `a`.
} The class If there is a breaking change to the interface of It's a software engineering decision which way to go. If class A {
...
int get anAThing => 24;
void anotherAThing(int i) {} // BREAKING CHANGE!
}
class B implements A { // `B` actually still implements `A`, so let's keep it.
final A a;
B(this.a);
void bThing() {}
int get anAThing => 42;
void anotherAThing(num n) => anotherAThing(n.toInt()); // Compensate for the breaking change.
* ==> a; // Forward all other members to `a`.
} We didn't get to the first task (writing a faithfully forwarding invocation in terms of lower-level mechanisms). This is mainly handled by mechanisms like |
@tatumizer wrote:
Perhaps the Rust folks haven't been thinking about this distinction? Anyway, we can play around with terminology as much as we want if we're willing to explain the meaning of every word as we write/speak it. If we want to communicate more effectively then it does make sense to try to stick to existing definitions.
Indeed, lots of things could be expressed using that kind of declaration. However, I chose For instance, |
I think we need a notion of a forwarding function which will faithfully invoke another function, as a semantic primitive in Dart. That is, we should specify once and for all what this is, and then we can use it in several different situations.
In each of these cases, a forwarding function with the semantics described here would solve some gnarly issues concerning default values. Moreover, explicitly declared forwarding functions can be useful in their own right as a modeling device: In some cases it is simply natural and convenient for an object to provide a service by forwarding the method invocation to some other object, and in those cases it will also be useful to know that there is no problem with default values.
Here are some ideas that we could use to get started.
Such forwarding functions can't be written in plain Dart without whole-program information, because a function could be a forwarder to an instance method, and the forwarding invocation would then have to be written using global information about which overriding concrete declarations the statically known method has, and which default values they are using for each optional parameter.
In particular, it does not specify any default values for any optional parameters: Invocations will use the default values specified by the forwardee, not the forwarder.
We could think about this as a kind of an interface/implementation relationship where the forwarder is similar to an interface element (e.g., an abstract instance method declaration in a class), and the forwardee is similar to the concrete method which is actually invoked at run time.
Note that the type of
f
can be a proper supertype of the type ofg
. For example,f
can have stronger constraints on the parameters (e.g., iff
has typeObject Function([int])
andg
isbool g([num n = 3.14])) => e
). The fact thatg
can have default values thatf
couldn't have if written as a regular function declaration (because they would be a type error) is another reason why we need a primitive mechanism to express forwarding functions.The forwarding function should satisfy a rather simple requirement at run time:
It follows that the effect of performing a generic function instantiation (
f<typeArgs>
) yields a function object that behaves the same as the generic instantiation of the forwardee function (g<typeArgs>
oro.g<typeArgs>
, respectively).We could consider allowing a given forwarding function object to rebind the forwardee to a different function. We could also consider allowing a forwarding function to forward to a function of type
dynamic
orFunction
. However, let's start with the simple and well-understood case where the forwardee has a statically known signature (for the library/static/local case), or it is a correct override of a statically known instance member signature.We could consider forwarding to a getter. This seems to be unnecessary, because there is nothing wrong with a hand-written
() => o.myGetter
, that's not a task that requires semantics that we can't express.We could consider equality of forwarding functions: Forwarders to the same library/static function could be equal if it is the same function and they have the same function type. Forwarders to local functions would only be equal if identical (two distinct forwarders could forward to local functions with different run-time environments). Finally, forwarders to instance methods could be equal if they have the same (identical) receiver object as well as the same member name, the same statically known declaration of that member name, and the same function type.
As long as we only wish to use this concept to clarify the semantics of torn-off redirecting factory constructors and noSuchMethod forwarders, there is no need to have syntax for it.
However, it seems very likely that we'd want to use this mechanism in a broader set of cases. So here's a possible syntax, just to have something concrete to talk about:
In some cases we might want to change the signature of a function slightly when we create a forwarding function. We could use syntactic marker (I'll use an ellipsis, just to have something concrete) to specify that the forwarding function has the same named parameter declarations as the forwardee, except for the ones that we've mentioned.
Let's say we start with this one:
We could then reduce the verbosity by using a forwarding function declaration and specify that most of the parameter list is the same in the forwardee and the forwarder. As before, we get the return type from the forwardee when it is not specified:
A partial specification of positional parameters would be possible as well: An ellipsis in the forwarding function parameter list after one or more positional parameters would stand for a copy of the positional parameters of the forwardee with a position that is higher:
It would be a compile-time error if the forwardee and the forwarder disagree on whether each of those positional parameters is optional.
The text was updated successfully, but these errors were encountered: