-
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
Anonymous methods #260
Comments
Brilliant, applause from me 👏 . You could also combine this with the cascade operator and have: object
..{ print(this) }
.methodOnObject() I wonder if one liner syntax is possible: string
.trimLeft()
.((it) => it + "!") |
This is in some ways similar to #43 (pipe-operator). You take the result of an expression and use it as the first argument to a following function, If we extend #43 with The feature makes the block work as a function body, so |
@tatumizer wrote:
The point is that class A<X, Y> {}
class B<Z, W> extends A<W, Z> {} // Note the swap!
A<int, String> foo() => B();
main() {
var x = foo();
x.{ ... }; // How would we access Z, W, X, Y here?
} It would surely be too hard to read (and error prone) if we were to say that It would also be quite confusing if we just specify that we wish to access the type variables under the names So if we want to allow the anonymous method to have access to the actual type arguments of the receiver it must be at a specified type, and those type parameters will have to be declared explicitly.
That's a trade-off, of course. My intention was to insist on the association between implicit member access and the reserved word It is true that we may now need to think a little more in order to understand what We could use a different name like // Version 5, using an explicit `it` to denote the receiver.
void beginFrame(Duration timeStamp) {
// ...
ui.ParagraphBuilder(
ui.ParagraphStyle(textDirection: ui.TextDirection.ltr),
).{
it.addText('Hello, world.');
it.build().{
it.layout(ui.ParagraphConstraints(width: logicalSize.width));
canvas.drawParagraph(it, ui.Offset(...));
};
};
ui.SceneBuilder().{
it.pushClipRect(physicalBounds);
it.addPicture(ui.Offset.zero, picture);
it.pop();
ui.window.render(it.build());
};
} The use of The point is that we could choose to introduce this kind of function literal, based on the similarity to anonymous methods, and they would of course be all about the implicit use of |
@kasperpeulen wrote:
That's the intention! (I'm going to work on the 'proposal' part later today), and also
That's rather tricky, because we'd need to disambiguate the end of the function body: myString.(it) => it + "!" Does that parse as |
@tatumizer wrote:
Adding in the semicolon that we will presumably need (until we settle on something in the area of optional semicolons): var x = 0.{ // receiver can be anything
// long computation
return result;
}; Right, this means that we could use anonymous methods to express "block expressions". I'm not sure it would be recommended in the style guide, but it's an interesting corner that I hadn't thought of. ;-) |
@lrhn wrote:
That's definitely an interesting idea! The pipe operator will surely call for the addition of multiple returns (such that we can do (e1, e2) -> (T x, S this){ ... } which would allow us to declare names for all the incoming objects (and, presumably, we would still be able to use We should be able to do this later even in the case where we set out with a more restricted model (exactly one incoming object). |
This looks similar to the argument blocks idea, but with a focus on mutable classes. It relies on the property of the implicit I think we should be very careful about introducing conveniences for mutables or other code that deals with side-effects. We do not want to discourage immutability even further. |
@tatumizer wrote:
In JavaScript there are a bunch of reasons why the meaning of In Dart we have a well-defined notion of lexical scoping, and the meaning of So, granted, it does make |
@tatumizer wrote:
True, it does add up quite nicely! |
The question here is what the goal of the operation is. If it is to make implicit If the goal is to be able to introduce something that otherwise works as a method on an object, locally, then I think it's a red herring. A method added on the side can't do anything that a simple function can't, except for the Having a number of statements in an expression context is also not new, an immediately applied function literal will do that for you. What would be new and useful is a way to introduce a statement in expression context without entering a new function body, allowing control flow in and out of the expression. That's probably also a tall order implementation-wise (I'm sure the VM assumes that an expression cannot break, continue, return or yield, and allowing that likely won't be worth the effort). So, all in all, the only thing really useful I see here is a way to locally rebind |
@yjbanov wrote:
I proposed similar things already a couple of years ago and it's definitely possible that these ideas blend in with each other over time. I actually thought that the named argument blocks would be intended to allow for something like passing named arguments (using a sequence of assignment-ish constructs) rather than being regular blocks (containing full-fledged statements), but it's certainly not hard to see the similarities.
I want to encourage immutability, and support developers in knowing and enforcing it. We would presumably do this by adding support for value classes, and I hope we'll get around to adding that soon. However, I don't think my proposal here is particularly strongly tied to mutable state. It is true that it is concerned with the provision of a scope where statements can be executed, and statements are frequently used to specify computations with side effects. There's nothing that prevents you from using an anonymous method with a receiver which is immutable. It just means that the available methods on that object won't mutate it. For instance, you might want to use the methods in order to extract various parts of the immutable state of the receiver and passing them on to functions.
I would expect this to be an obvious use case for anonymous methods (and they would presumably fit in quite well with the But I guess the main point you are making is that we should have at least similarly good support for the creation of immutable objects as the support that we have for building an object graph of mutable objects (using anonymous methods, or whatever we have). I think this would mainly be concerned with the ability to pass complex arguments in an argument list that would be used to build a new immutable object, and the ability for developers to read that kind of code. We do have Flutter's widespread usage of named arguments and a trailing comma based formatting for that, but I do recognize that we might want improvements here as well. |
@lrhn wrote:
If Of course, we could use
That would indeed be useful, because it would allow us to use But, as you mention, that might also have some more complex implications for the implementation. The anonymous methods that I proposed here should be rather easy to implement (the feature spec already hints that they could be mostly syntactic sugar). |
Indeed my rule for whether a specialized construct is worth it when a general construct already enables the functionality is that:
This comparison is also applied against potential features, not just existing ones. If we can add another good feature which is more general, and makes the improvement of this feature insignfiicant, then we should probably do that instead. For the Going to Making the nested block look like a method does not give any significant advantage, it's the localized change of Since the "method" has access to local variables in a different function, it's not really a method anyway, it's a different syntax for a function literal (including the inferral of return type) which is immediately invoked, and with the option of nameing the parameter Another option is to allow any function to denote one of its positional parameters as |
I've explored the syntactic implications of allowing Kotlin-ish abbreviated function literals (like |
@lrhn wrote:
I think it's useful to allow for the syntactic reorganization whereby a complex expression (say, a constructor invocation with many arguments) can be pulled up in front of a portion of code where the resulting object can be accessed by name. Anonymous methods will do that, and so will abbreviated function literals (#265) along with the pipe operator. I also think it's useful to let such a locally-prominent object be accessed implicitly by binding @lrhn, do you have a good solution for that? And how about cascades of anonymous methods, do we need a cascade pipe? I'm not totally convinced that the following two forms will be considered equally convenient: void foo() { ... } // Not an instance method.
main() {
// 1.
var x1 = SomeClass(); // with `add..` methods.
x1 -> (this){ addSomething(); foo(); addSomethingElse(); }();
// 2.
var x2 = SomeClass()..{ addSomething(); foo(); addSomethingElse(); };
} I suspect that the situations where we will want to work on an anonymous object will arise "often enough", taking the clue that Kotlin uses exactly this combination (binding of This is the primary reason why I chose the Flutter example where some non-trivial object building is being done. Of course, there would be other cases as well, but it seems obvious that we might want to avoid creating a lot of names for objects that we are just touching briefly, because they are being configured and then added to an object graph that we are currently building. Finally, I tend to think that anonymous methods do fit into the language. YMMV. ;-) Of course, I already proposed to make the provision of a binding of I don't actually see much of a problem in defining
I don't really buy that. The point is that implicit access to members of the type of In contrast, I suspect that the ability to introduce implicit member access for an arbitrary parameter of an arbitrary function would make the code considerably harder to read, because there is no simple mental model that developers can rely on, like saying "this is like a method on object o". So we should probably not be extremely permissive when we decide how to support binding of |
In #267, I've explored the foundations of the binding of |
@tatumizer wrote:
Interesting idea, thanks! I wouldn't necessarily want to prevent receivers of type |
@tatumizer wrote:
Right. @lrhn already described the conceptual reason why we decided to make implicit extension method invocations on a It's tempting to ask why all those receivers are dynamic in the first place, but I suspect that it would not work well enough to change all type annotations to avoid But that's basically because this is a job for 'extension types' (#42), that I prefer to call 'views' or 'view types' because they are concerned with viewing a specific loosely typed data structure as having a strictly specified typing structure, and enforcing that it is used accordingly. There is no run-time representation of the view, so it's a zero-cost abstraction, not unlike a Haskell The idea is that a loosely typed data structure with root v (say, modeling json values, consisting of objects of type In summary, I think it's more promising to address this whole area by means of view types, and then the special case of invoking static extension methods on receivers of type |
Here is an example showing that anonymous methods allow us to use a style which is rather similar to the one used in Kotlin with "UI as code". In the example, we're building a tree using anonymous methods. A class instance extension member is used in order to allow the addition of a tree node to its parent in a concise way, again similar to the way it's done in Kotlin (we're using operator class Tree {
final String value;
final List<Tree> children = [];
Tree(this.value);
String toString() =>
'$value(${[for (var c in children) c.toString()].join(', ')})';
void (Tree t).operator ~() => children.add(t);
}
Tree build({required bool third}) {
return Tree('n1').{
~Tree('n11');
(~Tree('n12')).{
~Tree('n121');
~Tree('n122');
};
if (third) ~Tree('n13');
};
}
void main() {
print(build(third: true)); // 'n1(n11(), n12(n121(), n122()), n13())'.
} The main point is that we're building a tree simply by "mentioning" the subtrees (with the This is possible because the The parentheses around We could also use a getter. This time we'll return the syntactic receiver, just to see how it looks: class Tree {
...
Tree get (Tree t).add {
children.add(t);
return t;
}
}
Tree build({required bool third}) {
return Tree('n1').{
Tree('n11').add;
Tree('n12').add.{
Tree('n121').add;
Tree('n122').add;
};
if (third) Tree('n13').add;
};
} We could also avoid the class instance extension members entirely, and use a regular class Tree {
...
void add(Tree t) => children.add(t);
}
Tree build({required bool third}) {
return Tree('n1').{
add(Tree('n11'));
add(Tree('n12')..{
add(Tree('n121'));
add(Tree('n122'));
}); // This `)` is far away from its `(`.
if (third) add(Tree('n13'));
};
} Note that we use There are many options, but the use of a prefix operator seems to be rather pragmatic, especially if the grammar is adjusted such that anonymous methods have a lower precedence (just below unary operators, presumably). |
In response to #259, this issue is a proposal for adding anonymous methods to Dart.
Edit: We could use the syntax
e -> {...}
in order to maintain syntactic similarity with the pipe operator proposed as #43, as mentioned here. See #265 for details. Later edit: Changed generic anonymous methods to be a separate extension of this feature, such that it is easier to see how it works without that part.Examples
The basic syntax of an anonymous method is a term of the form
.{ ... }
which is added after an expression, and the dynamic semantics is that the value of that expression will be "the receiver" in the block, that is, we can access it usingthis
, and methods can be called implicitly (sofoo(42)
meansthis.foo(42)
, unless there is afoo
in the lexical scope).To set the scene, take a look at the examples in #259, version 1 (original), 2 (unfolded), and 3 (folded). Here is a version of that same example that we could write using anonymous methods:
In this version the emphasis is on getting the complex entities (the objects that we are creating and initializing) established at first. With the given
ParagraphBuilder
in mind, we can read the body of the anonymous method (where we add some text to that paragraph builder and build it). We continue to work on the paragraph returned bybuild
, doing alayout
on it, and then using it (this
) in the invocation ofdrawParagraph
.Similarly, the second statement gets hold of the scene builder first, and then works on it (with three method invocations on an implicit
this
, followed by one regular function call torender
).Compared to version 3, this version essentially turns the design inside out, because we put all the entities on the table before we use them, which allows
drawParagraph
andrender
to receive some much simpler arguments than in version 3.Compared to version 2, this version allows for a simple use of statements, without the verbosity and redundancy of using names like
paragraphBuilder
many times. This means that we can use non-trivial control flow if needed:We can of course also return something from an anonymous method, and we can use that to get to a sequential form rather than the nested one shown above:
We can choose to give the target object a different name than
this
:We can use a declared type for the target object in order to get access to it under a specific type:
Note that this implies that there is an implicit check that
xs is Iterable<int>
(which may be turned into a compile-time error, e.g., by using the command line option--no-implicit-casts
). Also, the addition of3
toxs
is subject to a dynamic type check (becausexs
could be aList<Null>
).As a possible extension of this feature, we can provide access to the actual type arguments of the target object at the specified type:
We are using type patterns, #170, in order to specify that
X
must be bound to the actual value of the corresponding type argument forxs
. We also specify thatX
must be a subtype ofnum
, such that the body of the anonymous function can be checked under that assumption, and this is a statically safe requirement because the static type ofxs
isList<num>
.Finally, if the syntax works out, we could extend this feature to provide a new kind of function literals. Such a function literal takes exactly one argument which is named
this
, and the body supports implicit member access tothis
, just like an instance method and an anonymous method:use an anonymous method as an alternate syntax for a function literal that accepts a single argument (and, of course, treats that argument as
this
in its body):Proposal
This is a draft feature specification for anonymous methods in Dart.
Syntax
The grammar is modified as follows:
Static Analysis
It is a compile-time error if a formal parameter is named
this
, unless it is a parameter of an anonymous method or a function literal.An anonymous method invocation of the form
e.{ <statements> }
ore..{ <statements> }
is treated ase.(T this){ <statements> }
respectivelye..(T this){ <statements> }
, whereT
is the static type ofe
.An anonymous method invocation of the form
e?.{ <statements> }
is treated ase?.(T this){ <statements> }
whereT
is the static type ofe
(with non-null types,T
is the non-null type corresponding to the static type ofe
).The rules specifying that an expression
e
starting with an identifierid
is treated asthis.e
in the case whereid
is not declared in the enclosing scopes remain unchanged.However, with anonymous methods there will be more situations where
this
can be in scope, and when an anonymous method is nested inside an instance method, the type ofthis
will be the type of the receiver of the anonymous method invocation, not the enclosing class.In an anonymous method invocation of the form
e.(T id){ <statements> }
ore..(T id){ <statements> }
, it is a compile-time error unless the static type ofe
is assignable toT
. (Note thatid
can bethis
.) Moreover, it is a compile-time error ifT
isdynamic
.It is a compile-time error if a statement of the form
return e;
occurs such that the immediately enclosing function is an anonymous function of the forme..(T id){ <statements> }
. This is because the returned value would be ignored, so the return statement would be misleading.The static type of an anonymous method invocation of the form
e.(T id){ <statements> }
is the return type of the function literal(T id){ <statements> }
. The static type ofe?.(T id){ <statements> }
isS?
, whereS
is the return type of the function literal(T id){ <statements> }
.The static type of an anonymous method invocation of the form
e..(T id){ <statements> }
is the static type ofe
.Dynamic Semantics
Evaluation of an expression of the form
e.(T id){ <statements> }
proceeds as follows:e
is evaluated to an objecto
. It is a dynamic error unless the dynamic type ofo
is a subtype ofT
. Otherwise,(T id){ <statements> }(o)
is evaluated to an objecto2
, ando2
is the result of the evaluation.Evaluation of an expression of the form
e?.(T id){ <statements> }
proceeds as follows:e
is evaluated to an objecto
. Ifo
is the null object then the null object is the result of the evaluation, otherwise it is a dynamic error unless the dynamic type ofo
is a subtype ofT
. Otherwise,(T id){ <statements> }(o)
is evaluated to an objecto2
, ando2
is the result of the evaluation.Evaluation of an expression of the form
e..(T id){ <statements> }
proceeds as follows:e
is evaluated to an objecto
. It is a dynamic error unless the dynamic type ofo
is a subtype ofT
. Otherwise,(T id){ <statements> }(o)
is evaluated to an objecto2
, ando
is the result of the evaluation.Discussion
As mentioned up front, we could use
->
rather than.
to separate the receiver from an associated anonymous method, which would make this construct similar to an application of the pipe operator (#43).This might be slightly confusing for the conditional variant (where we would use
? ->
rather than?.
) and the cascaded variant (where we might use-->
rather than..
, and? -->
rather than?..
, if we add null-aware cascades).It might be useful to have an 'else' block for a conditional anonymous method (which would simply amount to adding something like
('else' <block>)?
at the end of the<anonymousMethodSelector>
rule), but there is a syntactic conflict here: If we useelse
then we will have the combination of punctuation and keywords (e?.{ ... } else { ... }
). Alternatively, if we use:
then we get a consistent use of punctuation, and we get something which is similar to the conditional operator (b ? e1 : e2
), but it will surely surprise some readers to have:
as a larger-scale separator (ine?.{ ... } : { ... }
, the two blocks may be large).Note that all other null-aware constructs could also have an 'else' part, specifying what to do in the case where the receiver turns out to be null, such that the expression as a whole does not have to evaluate to the null object.
Note that we could easily omit support for
this
parameters in function literals, or we could extend the support to even more kinds of functions.We insist that the receiver type for an anonymous method cannot be
dynamic
. This is because it would be impractical to let every expression starting with an identifier denote a member access on that receiver:However, another trade-off which could be considered is to allow a receiver of type
dynamic
, but give it the typeObject
in the body.The text was updated successfully, but these errors were encountered: