-
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
Forwarding function expressions and declarations #1880
Comments
I was surprised to find out even this doesn't work: class A {
void f(int i, int j, [int k = 0]) => print("A.f");
}
abstract class B {
void g(int i, int j, [int k = 0]);
}
class C implements B {
final A a;
C(this.a) : g = a.f;
// Error: Can't declare a member that conflicts with an inherited one
// Info: Annotate overriden members
Function g;
} Especially since these two are equivalent: class D {
void a() => print("Hello");
final b = () => print("Hello");
}
void main() {
final d = D();
d.a(); // "Hello"
d.b(); // "Hello"
} On the one hand, Dart can invoke a field that has a Function type, but on the other hand, it doesn't recognize such a field as a method, even when the context would suggest it is. Even if this did work, we'd still lose info about parameters and return types on class A:
def f(self, i, j, k=0): print(i, j, k)
class C:
def __init__(self, a):
self.a = a
self.g = a.f
c = C(A())
c.g(0, 1) # 0 1 0
c.g(0, 1, 2) # 0 1 2 Maybe instead of |
Dart has always had the rule that it is a compile-time error to override a method with a getter or vice versa, even though the types can be matched up as you show. I think one of the main reasons for this basic rule was that it lowers the performance of the language as a whole if every method invocation may turn out to be a getter invocation, yielding a function object, followed by an invocation of that function object. We generally don't know that a method hasn't been overridden in the dynamic type of any given receiver, so it doesn't help that the denoted member statically resolves to a method. |
Makes sense. I'm just trying to think of a more intuitive way besides g(-) = forwardTo(a.f); Would this work? g => a.f; It's currently invalid syntax ( void g(int i, int j, int k, {bool isFoo = false}) => a.f(i, j, k); |
I think it would be difficult to specify the signature with the |
You're right, my example was focusing too much on when class A {
void b(int i, int j, {int k=0}) => print("$i, $j, $k");
}
class B {
final A a;
const B(this.a);
void c(int i, int j, {required int k, bool debug = false}) : a.f
// translates to:
void c(int i, int j, {required int k, bool debug = false}) => a.f(i, j, k: k);
} EDIT: I'm actually more for class A {
void b(int i, int j, {int k=0}) => print("$i, $j, $k");
}
class B {
final A a;
const B(this.a);
void c(int i, int j, {required int k, bool debug = false}) = a.f
// translates to:
void c(int i, int j, {required int k, bool debug = false}) => a.f(i, j, k: k);
} |
@tatumizer, my impression of this proposal is that it's only for delegates, meaning the actual result of the forwarded call is not used further in the body Here's the example in issue #418: class House {
num setDimensions(num north, num south, num west, num east) =>
north + south + west + east;
}
class HouseManager {
final House house;
const HouseManager(this.house);
num setDimensions(num north, num south, num west, num east) =>
house.setDimensions(north, south, west, east);
} In this case, the above can be rewritten to something like: class HouseManager {
final House house;
const HouseManager(this.house);
num setDimensions(num north, num south, num west, num east) = house.setDimensions;
} |
@Levi-Lesches - I think the syntax class B {
final A a;
const B(this.a);
void c(int i, int j, {required int k, bool debug = false}) = a.f;
} It may or may not be useful to use a syntax that includes a very explicit indication of the nature of this declaration (by having the word If we assume that we don't need the word typedef G = void Function(int, int, {required int k, bool? debug});
void f(int i, int j, {int k=0}) => print("$i, $j, $k");
g(-) = forwardTo<G>(f); |
Sorry, I'm a bit lost, are you trying to find a way to get rid of the argument list? If so, what about the class A {
void b(int i, int j, {int k=0}) => print("$i, $j, $k");
}
class B {
final A a;
const B(this.a);
void c(...) = a.f;
// translates to
void c(int i, int j, {int k=0}) => a.f(i, j, k: k);
} If you're talking about using typedef G = void Function(int, int, {required int k, bool? debug});
class A {
void b(int i, int j, {required int k}) => print("$i, $j, $k");
}
class B {
final A a;
const B(this.a);
void c(...) = a.f as G
// translates to
void c(int i, int j, {required int k, bool? debug}) => a.f(i, j, k: k); // debug is dropped
} |
@Levi-Lesches wrote:
I'm looking for a concise and automatic (hence less error-prone) mechanism to obtain a function of a given type based on forwarding to a function of a similar type. The current approach would be to write a function/method declaration or a function literal, spelling out the entire parameter part, with a body which is just a When it comes to the expression form, the mechanism offers support for signature adaptation. For instance, if you have a function You could of course also pass
We could certainly use My main worry is that I think forwarding declarations will be a rather small feature: It's going to pop up here and there, probably there will be some "delegation" classes where almost all methods are just forwarding to some other object, and this probably means that the syntax needs to be somewhat self-explanatory. The presence of the word |
So would the two above examples work? It seems to be able to handle both: 1) getting rid of long paramter lists (using |
I think they would work. I just tend to prefer to have the word |
How about typedef G = void Function(int, int, {required int k, bool? debug});
class A {
void b(int i, int j, {int k=0}) => print("$i, $j, $k");
}
class B {
final A a;
const B(this.a);
c forward => a.f // a "normal" forward
// translates to
void c(int i, int j, {int k=0}) => a.f(i, j, k: k);
d forward => a.f as G // forward with a cast
// translates to
void d(int i, int j, {required int k, bool? debug}) => a.f(i, j, k: k);
void e(int i, int j, {required int k, bool debug = false) forward => a.f; // explicit signature
// translates to
void e(int i, int j, {required int k, bool debug = false) => a.f(i, j, k: k);
} One benefit to doing it this way is that you probably don't need the parenthesis for the grammar to be unambiguous, so you don't need |
I like that! |
I adjusted the proposal to let typedef G = void Function(int, int, {required int k, bool? debug});
class A {
void b(int i, int j, {int k=0}) => print("$i, $j, $k");
}
class B {
final A a;
const B(this.a);
c(-) forward => a.f;
d(-) forward => a.f as G;
void e(int i, int j, {required int k, bool debug = false}) forward => a.f;
// translates to
void c(int i, int j, {int k=0}) => a.f(i, j, k: k);
void d(int i, int j, {required int k, bool? debug}) => a.f(i, j, k: k);
void e(int i, int j, {required int k, bool debug = false}) => a.f(i, j, k: k);
} I tried to use [Edit: Going over the proposal and updating various details, I decided to drop the support for using an explicit result type, as in |
Looks great! Just one detail: is it possible to switch out the |
Also, now that we're using As @tatumizer and others in the linked issues pointed out, a common use of forwarding is to either do something with the result or simply wrap the function. After all, if you can do class House {
num getPerimeter(num north, num south, num west, num east) =>
north + south + west + east;
}
class Contractor {
Future<void> orderMaterials(num amount) =>
Future.delayed(Duration(seconds: amount.floor()), () {});
}
class HouseManager {
final House house;
final Contractor contractor;
const HouseManager(this.house, this.contractor);
/// Computes the perimeter and orders the needed materials
Future<num> setDimensions(num north, num south, num west, num east) async {
print("Getting dimensions");
num perimeter = house.getPerimeter(north, south, west, east);
await contractor.orderMaterials(perimeter);
return perimeter;
}
}
setDimensions(...) forwards { // forwards indicates this signature needs to be expanded
print("Getting dimensions");
num perimeter = forward house.getPerimeter; // similar to awaiting a Future
print("Got perimeter: $result");
return perimeter;
} And with a different signature: Future<void> setDimensions(num north, num south, num west, num east, {bool shouldOrder = true}) forwards async {
print("Getting dimensions");
num perimeter = forward house.getPerimeter; // similar to awaiting a Future
if (shouldOrder) await contractor.orderMaterials(perimeter);
} In summary:
Again, this does add more complexity for the sake of saving a line or two, but just throwing it out there. |
@Levi-Lesches wrote:
I think that's a subjective decision (I don't expect any serious parsing problems with any of these), so we could certainly do that.
That does make sense, but it is a very delicate relationship. In particular I'm afraid that the code won't be sufficiently stable, maintainable, or readable, if we allow a The simple proposal where a |
Agreed, it's a lot more complexity than it's worth. Besides, when showing a new coder around a codebase, one can easily explain why they need one function to wrap another, but if signatures start changing based on the code inside said function, that starts to cause confusion. I think it's one of those things that makes sense when you expect it but can otherwise feels magical and arbitrary. |
The ability to concisely specify forwarding functions has been requested several times (e.g., #58, #59, #157, #418; and #493 requests a similar feature for constructors).
The main point is that it is verbose and error-prone to write a forwarding function, because it includes a full declaration of a parameter part (value parameters, with types, names, optionality, plus possibly type parameters, possibly with bounds) which is often very similar to the parameter part of that other function which is the forwardee. For example:
When the function types are identical it seems clearly suboptimal to have to write the signature again, and pass a suitable list of parameters as actual arguments. However, even with somewhat different function types, even in some cases where those function types are not subtypes of each other (in any direction, like in the example above), it is still possible to specify how the forwarding invocation should be performed.
Hence, this proposal contains rules for how to match up two function types that differ in several ways. It is of course trivial to eliminate this part of the proposal and simply require that we are dealing with two identical function types; in that case the expression form is probably useless and can be omitted as well.
Syntax
This proposal introduces a new function body and a new formal parameter list:
Static analysis and semantics
We propose that this mechanism should be made available for declarations and for function literals, such that we can specify a forwarding function concisely both in a declaration context and in an expression context.
We use
Function.forwardTo<G>(<expression>)
as the concrete syntax for the expression case, and a function body of the formforward => <expression>
for the declaration case.We use
(-)
as a parameter part in a function declaration to indicate that the signature is obtained from some other source (e.g., in an instance method declaration that signature could be obtained from a superinterface). If the return type is provided then only the formal parameters are obtained from that other source. For example:The meaning of the declaration of
C.g
is defined by desugaring. First, the function signature (return type and formal parameters, with all properties) are obtained from the interface ofB
, which yields the following declaration (that we could also have written in the first place, if preferred):A default value for an optional parameter may be needed; it is obtained from the declaration that provides the signature, if that yields a unique result; otherwise, it is obtained from the corresponding parameter in the forwardee, if present; otherwise a compile-time error occurs.
Next, we determine two function types
F
andG
:G
is the type of the forwarding function, that is, the function type of the instance methodg
; so that'svoid Function(int, int, int, {bool isFoo})
.F
is the type of the value argument, that is, the static type of the instance method tearoffa.f
, so this isvoid Function(int, int, [int])
.The semantics of a function body of the form
forward => e;
is defined as a desugaring step which replaces this body by an arrow body of the form=> e<X1..Xm>(args)
whereargs
is a list of actual arguments which is computed as specified below, based onG
andF
, andX1..Xm
is a list of type arguments which is the type parameters ofG
, or a prefix thereof (such thath
receives the number of type parameters that it declares).A compile-time error occurs unless
e
has a static type which is a function type.It is a compile-time error if the declaration is static and
e
denotes an instance member; it is a compile-time error if the declaration declares an instance method or a static method, ande<X1..Xm>(args)
is a compile-time error. Finally, it is a compile-time error unless we have the relationG <:: F
, which is specified in the following.The semantics of the expression form
Function.forwardTo<G>(e)
is that it is desugared to a function literal that declares formal parameters and type parameters based on the type argumentG
, which specifies the type of the resulting function. A compile-time error occurs if any non-null default values are required in the resulting function literal. The body of the function literal which is the result of the desugaring invokese
ase<X1..Xm>(args)
, whereX1..Xm
is the list of type parameters ofG
or a prefix thereof, such that the number of type arguments passed toe
is equal to the number of type parameters declared by the static type ofe
. A compile-time error occurs unlessG <:: F
whereF
is the static type ofe
.We need to ensure that
args
is uniquely determined, which at first implies that it is possible to computeargs
at all. We introduce the binary type relationship<::
to do that; this relationship is also known as the wraps relationship.For example, we do have the property
G <:: F
ifG
is the type ofg
andF
is the type off
, as defined in the very first example.Let
G
andF
be function types such thatG
has nG positional parameters and kG required positional parameters, andF
has nF positional parameters and kF required positional parameters, and n is the minimum of nG and nF. The relationshipG <:: F
holds if each of the following is satisfied:G
declares at least as many type parameters asF
, and ifT1..Tm
is a list of actual type arguments that satisfy the bounds declared byG
, thenT1..Tl
, wherel <= m
, is a list of actual type arguments that satisfy the bounds ofF
. So we can drop some type arguments, but we will not invent any. In the remaining bullets we assume that we are passing thel
first type parameters ofG
as the actual type arguments toF
.F
is assignable to the return type ofG
.G
has enough positional parameters to be able to call anF
".G
, where 1 <= j <= n, the type of pj is assignable to the _j_th positional parameter ofF
.F
, there exists a named parameter p ofG
, and the type of the latter is assignable to the type of the former.G
where there exists a named parameter q ofF
with the same name, the type of the former is assignable to the type of the latter.We can now specify the actual arguments
args
that are passed in the implicitly induced forwarding invocation: It contains the positional parameters pj ofG
, where 1 <= j <= n, in that order; moreover, it contains an actual argument of the formn: n
for each named parameter with the namen
which is declared by bothG
andF
.The names of positional parameters in
G
are fresh identifiers. The point is that the first positional parameter of the wrapper function is passed as the first positional parameter of the wrappee, and similarly for the second, etc, and there is no user written code which is able to refer to the positional parameters.Discussion
Use type inference?
We could consider specifying the desugaring of the forwarding function literal and the forwarding function body such that it simply invokes
h(args)
whereh
is the value argument offorwardTo
and the argumentsargs
are as specified above. In this case we would leave it to the type inference process to come up with suitable actual type arguments in the invocation of the forwardee, if any.This may be somewhat hard to handle from the outside, though, because we would have to make the success or failure of this type inference step part of the wraps relation (
<::
), and this could make expressions usingforwardTo
unstable in ways that are hard to understand, as part of software development and evolution. For instance, we might rundart pub upgrade
, and then a tiny difference in typing cause one of these type inference steps to fail, and then an expression likeFunction.forwardTo(f)
is suddenly an error, and it takes some work to track down how that happened.Usage as an IDE feature
It seems likely that we would want to allow for variation in the implicitly induced invocation of the wrappee in the body of the new function declaration or the wrapper function literal.
However, it may be sufficient to provide a quick-fix that expands any given use of
forwardTo
in a declaration orFunction.forwardTo
in an expression, just like the compiler would do it anyway during compilation.This would then allow developers to get the function declaration or function expression right when it is introduced, and henceforth it can be manually desugared and edited as needed, which means that the body could be changed to a block, and it could contain arbitrary code.
Higher-order constructs?
In order to express higher-order constructs, it might be useful to be able to abstract over the wraps relation.
However, this kind of generalization can hardly be supported, because the construct
Function.forwardTo...
and the declarations with body= forwardTo...
are only capable of covering the situation where the given function types are fully known at compile time.Versions
forward
be similar toasync
/sync*
/async*
).The text was updated successfully, but these errors were encountered: