-
Notifications
You must be signed in to change notification settings - Fork 213
Problem: Wrapping functions and forwarding arguments is noisy #157
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
Comments
See also https://api.dartlang.org/stable/dart-async/Zone-class.html We have three flavors of many methods |
For what it's worth, there is no simple solution to this issue (which is probably why no-one has commented). It's easy to make a function that returns something of the same type as the argument — if it returns the argument. Any solution would have to be very dynamic (going on reflective). For example, if we added a helper function like: /// Creates a function of type `T`.
///
/// When the created function is called, the [callback] function is called with
/// a list of the positional arguments and a map of the named arguments that
/// were actually passed in the call. The return value of the [callback] function
/// becomes the return value of the returned function's invocation,
/// and must have the correct type.
external T createFunctionProxy<T extends Function>(dynamic callback(
List<dynamic> positionalArguments,
Map<Symbol, dynamic> namedArguments)); This would be just as dynamic as a A simpler solution, which might solve 95% of the issues, but not be as hard on the /// Creates a new function with the same type as [function].
///
/// When the created function is called, the [before] function is called first,
/// before the parameters are passed to [function]. If [before] throws,
/// the [function] is not called.
/// When the call to [function] returns a value, the [after] function is called
/// before the result is returned to the caller.
T wrapFunction<T extends Function>(T function, {void before()?, void after()?}); This would be much more type-safe. (Whether the returned function will have type Anything that requires intercepting arguments is untypabel. |
I hope there is something more static we could do here. As a strawman, imagine something like: T Function(<*args*>) runWithOverrides<T, <*args*>>(T Function(<*args*>) callback) {
return (<*args*>) {
return runZoned(() => callback(<*args*>),
zoneValue: {someZoneKey: someZoneValue});
};
} It shouldn't matter the runtime type of Real motivating examples I've seen so far don't care about doing anything with the arguments except to forward them without modification. They may care about modifying the return value, but we already have ways to express that with generics. One motivating example is linked above - it would be nice in This use case would likely need to change a Another is in I think in other issues I've seen @munificent mention how there are parallels between an argument list and some notion of a "Tuple" that has both named and positional values. I'm not sure if that could help here. |
I'd disagree. An example would be React/Redux, which injects the store's state through function currying. I think the main difficulty is because Dart lacks both tuples and records. So the prototype of Result apply<Result, Positional, Named>(
Result Function(...Positional, {...Named}) function,
Positional positionalParameters,
Named namedParameters,
); |
Abstracting over argument lists is an idea. Without destructuring, they'll necessarily be opaque, but if you can reapply them at a point where the static type is known to match, then it could work. It's true that tuples can be generalized to contain named entries as well as indexed ones, and that parameter types are tuple types, formal parameters are tuple destructuring, and actual arguments are tuple values. If we allowed a general supertype over all tuples, then a function type would be That could work (and does work in functional languages like SML). Without such an abstraction, we'd have to introduce a new kind of type variable to range over parameter signatures and allow captured argument lists to have that type, but otherwise be mostly opaque (because if not, we'll have introduce tuples anyway). If we have tuple spreads, and partial tuple destructuring, then we can do all sorts of things with functions, including currying at arbitrary arities: R Function(...P2) Function(...P1) curry<R, P1 extends Tuple, P2 extends Tuple>(
R Function(...P1, ...P2) f) => (...P1 t1) => (...P2 t2) => f(...t1, ...t2); (Here I have no idea how inference will work. |
Tuple types params and tuple type destructuring would be amazing! I can imaging that inference would be difficult though, either P1 or P2 would have to be specified in your example, unless you had a context type for the returned function. If you could use . or something as a placeholder for inference you could do something like this. final myFunc = curry<int,.>((int d, int c, String g) => '$d $g $c');
final newFunc = myFunc(10);
print(newFunc(20, 'hi')); // outputs: 10 hi 20 |
It could work, but it won't come together as naturally as it does in SML where you don't have subtyping to contend with and where the language was designed around tuples from day one. Consider: void oneOrTwo(Object a, [int? b]) {
print('$a $b');
}
oneOrTwo(1, 2); I presume that would print I think that implies that we don't want to implicitly unpack argument lists either. So this:
Would print We could try to look at the type of the parameter list to decide whether to implicitly unpack: takeTwo(int a, int b) => print('$a $b');
takeTuple((int a, int b)) => print('$a $b');
var tuple (1, 2);
takeTwo(tuple); // Prints "1 2".
takeTuple(tuple); // Prints "1 2". But that can get weird: takeTwoOrTuple<T>(T a, [int? b]) => '$a $b';
callThing<T>(T arg) => takeTwoOrTuple<T>(arg, 2);
callThing(1); // "1 2"?
callThing((1, 3)); // "(1, 3) 2" or "1 3"? I don't think we want to specialize generics. The take away from all of this is that Dart probably does still need a fundamental notion of a parameter list for functions, which is distinct from tuple types. A 2-parameter function is a different type from a function that takes a single 2-tuple. To go from one to the other, users will need to explicitly pack and unpack. The former is just a tuple expression, and the latter is some kind of argument list spread operator, which I think is tractable and really useful. It's just not automatic or implicit. Just like how list flattening in list literals isn't automatic. You have to request it by using |
@munificent The advantage of not making tuples into objects is that they won't have identity, and you can unbox and box as you see fit. You can have a parameter passing convention where a function returning a two-tuple puts both values on the stack. The disadvantage is that you can't abstract over all types any more. A function returning a two-tuple is not related to a function returning an So if you'd still have a super-type of all types, say Speed vs. convenience. I'm fairly sure non-Object tuples can be made more speed efficient. Possibly at a space-cost if we need to specialized too much code for AoT. (And then |
Ah, true, I suppose we could try to treat them like real primitive value types. It would feel very strange to do that when even numbers aren't primitive values in Dart, but I guess it's a thing we could do. My hunch is that the complexity that it would force onto users outweighs the performance benefits we'd get, but I could be wrong. |
I think dart could use a primitive value type, and I think tuples could be a good candidate. I don't see tuples as requiring the normal class things ( |
@munificent Agree. If tuples are the only value-types we introduce, it probably won't be worth it. |
It might mean things like:
Stuff like that. |
Tangentially, #1880 introduces a new syntax for the simple case of forwarding a function (wrapping is a little harder). It desugars an empty signature into the return type and parameter list of the specified function while avoiding tuples and other dynamic mechanisms. |
It's not possible to write a signature expressing that the return value is a function having the same argument count and types as one of the parameters. This leads to ugly blocks where we define the same function N times with varying argument counts and lots of duplication in generic types and forwarding the arguments.
For example
expectAsync0
throughexpectAsync6
: https://github.com/dart-lang/test/blob/ac21330338e0a7ad68fb19058bb92260706dd6cf/pkgs/test_api/lib/src/frontend/expect_async.dart#L295This gets even more complex with things like optional arguments.
The text was updated successfully, but these errors were encountered: