-
Notifications
You must be signed in to change notification settings - Fork 209
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
Support cloning functions #4281
Comments
We probably could also look into a better syntax than class Example {
#guard
#logged
int method(String value) => 42;
} But that's a bit out of the scope of this issue. |
The biggest immediate issue is the typing. // A new primitive, similar to Invocation but typed.
abstract class ArgumentList {}
abstract class TypedFunction<ReturnT, ArgsT extends ArgumentList> { The "similar to Invocation, but typed" skips all the hard parts. How is it typed? What is it even? Is there a whole type hierarchy similar to record types, but with each parameter also being able to be optional? If you want to abstract over parameter lists using generics, then parameter lists need to be types. If Dart didn't have optional parameters, it would be much easier. The parameter list type would just be a record type, and every function is a unary function that takes a single record value (and then the parameter list implicitly pattern matches it). I think it would be much more likely to introduce a "function wrapper" function where you never get access to the parameters, and therefore never need to know their "type". /// Creates a function of with type `F`, which must be a function type (not `Function` or `Never`).
///
/// When the returned function is invoked with an argument list, the [body] is invoked with a function
/// which must be invoked. When that function is invoked with a function argument,
/// that function is invoked with the same argument list as the returned function, and whatever
/// value it returns is memorized as the result of the returned function invocation.
/// If that result is a future, then the invocation returns a future which won't complete until
/// the result future has. Otherwise it returns `null`.
F functionProxy<F extends Function>(void Function(Future<void>? Function(F)) body); Example use: F logBeforeAfter<F extends Function>(String before, String after, F function) =>
functionProxy<F>((invoke) async {
print(before);
try {
if (invoke(function) case var delay?) await delay;
} finally {
print(after);
}
}); Async just makes things extra complicated! It would be a little easier to work with if you could just abstract over the return type of a function. |
#4271 can be extended support this use case: F wrap<@konst F extends Function>(F cb, void Function() before, void Function() after) {
final typeInfo = TypeInfo.of<F>();
return fun((positional, named, types) {
before();
final result = invoke(cb, positional, named: named, types: types);
after();
return result;
});
} An extension to what is already contained in #4271 is a primitive to construct functions: /// [R] is a return type of [F].
F fun<@konst F extends Function>(@konst Object? Function(List positional, Map<String, Object?> named, List<Type> types) handler)
```.
Though there are some challenges around achieving very precise typing because function type can't be deconstructed. |
I covered it in later snippets: Consider: String nullary() => 0;
int unary(int a) => a;
int complex(int a, {required String b, double c = 42, bool? d}) => 42; Those functions would implement TypedFunction<String, _()> a = nullary;
TypedFunction<int, _(int)> b = unary;
TypedFunction<int, _(int, {required String b, double c = 42, bool? d})> b = complex; I purposefully used Basically The idea is, given It'd behave like how _([int a]) optionalArg;
_() emptyArgList = optionalArg; I don't think we'd have a literal for manually creating |
Ack. So it is a complete type hierarchy which is more complicated than records, but which contains a subset that is similar to records. The types are the type of function parameter lists, not argument lists (using the traditional naming). An argument list value is assignable to a number of parameter list types. But if parameter list types are the types of argument lists, then they are covariant. It will be the That does mean that: _([int a]) optionalArg = ...
_() emptyArgList = optionalArg; would be a compile-time error. So something like:
and then Then Or we could allow type spreads and write it as R Function(P) logAround<R, P extends ParameterList>(String before, String after, R Function(...P) function) => (...P args) {
print(before);
try {
return function(...args);
} finally {
print(after);
}
}; |
There's a parallel thread #3444 which, upon some slight generalization in this comment, addresses the same problem with a much simpler solution. |
Can this ignore arguments if I call a different function that accepts fewer arguments? |
I think there's a very interesting design space here, about the creation of functions that are based on existing functions. An overall description of the situation is that we wish to create a wrapper function based on a given wrapped function, aka the wrappee. We might want to change the actual arguments in some way, or we might want to modify the returned value, or we might want to perform some actions before and/or after the invocation of the wrapped function, or we might even want to decide whether the wrapped function should be invoked at all, or whether it should be invoked repeatedly, perhaps bounded by some condition. That is, we basically want to be able to write general code. However, we also need to abstract away from the formal parameter declarations and the actual argument lists, because we'd like to make this wrapping setup work for many different function types. When it comes to the forwarding invocation itself, we presumably need a semantics which will ensure that the wrappee is called "in the same way as the wrapper was called" (except when the invocation is explicitly modified, e.g., by passing other actual arguments for some of the parameters). The standard example to show that this matters is the following: 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; // This means that `foo` forwards to `a.foo`.
}
void main() {
var c1 = C(B1()), c2 = C(B2());
print('Should be 1: ${c1.foo()}, should be 2: ${c2.foo()}');
} Naively, we could try to write the forwarding declaration That's the reason why I'm arguing in #3444 that we need a new primitive which will support forwarding invocations where the default values are preserved correctly. If we have support for faithful forwarding invocations then we can start generalizing the mechanism such that we can introduce all those modifications of the invocation (changing the values of some actual arguments rather than just forwarding all of them, choosing whether or not to call the wrappee at all, etc). Another dimension that we should keep in mind is the distinction between run-time operations and compile-time operations. For example, @mraleph's approach is quite powerful and convenient, and it might be possible to generalize it such that it can express true forwarding invocations (otherwise that would still need to be a new primitive, which could then be used by this kind of meta-programming declarations). This is an approach that relies entirely on compile-time information. Another (very different) approach could be to support a primitive Obviously, the compile-time mechanism can be checked statically. However, the run-time mechanism can also be checked statically in that it is known that In any case, this is indeed a very interesting topic and I'm sure we can come up with lots of good ideas. ;-) |
One common programing pattern is to have a reusable utility which wraps a function to perform logic before/after and to transform the result.
A typical example is a logger middleware. One might write:
Then used as:
The problem is:
Our
logged
util is bound to theT Function()
prototype. It does not accept any other Function variants.If we want a
Example.method
which takes parameters, we have to write a newlogged
; such asloggedUnary
/loggedBinary
/ ...This is a common issue. For example we have
Zone.current.bindCallback
vsZone.current.bindUnaryCallback
vsZone.current.bindBinaryCallback
...Proposal: Make all Functions have a
compose
method, and add aTypedFunction<Return, Arg>
interfaceThe idea is that all functions would implement a new interface defined as:
Consider:
Those functions would implement
TypedFunction
as followed:Note:
The second generic type in this snippet isn't a Record! It's a new type that represents a subclass of
ArgumentList
.Based on those interfaces, we can do:
This would log:
Example 1: Logging
Using this new feature, we could implement an all purpose
logged
utility:This could then be used as
Example 2: Wrapping functions in a
Result<T>
Consider:
We could implement a generic
guard
utility:This could then be used as
The text was updated successfully, but these errors were encountered: