Skip to content
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

Requst: Optional parentheses for one argument arrow function #320

Open
architkithania opened this issue Apr 18, 2019 · 13 comments
Open

Requst: Optional parentheses for one argument arrow function #320

architkithania opened this issue Apr 18, 2019 · 13 comments
Labels
feature Proposed language feature that solves one or more problems small-feature A small feature which is relatively cheap to implement.

Comments

@architkithania
Copy link

Arrow functions, or lambdas as some languages call it, are frequently used in dart, even more so in the Flutter framework. Most of these arrow functions only contain one argument. At the language's current standing, if I were to write a forEach loop on a List, I would have to do something like this:

void main(List<String> args) {
  List list = [1,2,3,4];
  list.forEach((item) => /* Do something item */);
}

forEach is just one of the many places that an arrow function is frequently used.
As you can see, the parentheses around the item make the whole arrow function a little awkward to write and also read. I propose the following change: make the parenthensis around one argument arrow functions optional, permitting writing code as such:

void main(List<String> args) {
  List list = [1,2,3,4];
  list.forEach(item => /* Do something item */);
}

This makes the code much cleaner to read and to write.

@architkithania architkithania changed the title Optional parentheses for one argument arrow function Requst: Optional parentheses for one argument arrow function Apr 18, 2019
@lrhn
Copy link
Member

lrhn commented Apr 29, 2019

I think this can be made into a fairly safe syntax change.

It likely won't affect readability, the => is syntax enough for the reader to recognize what is going on (although very long parameter names might make that harder to read, say x.forEach(wellFirstDoSomethingAndThenDoSomethingElse => print(well....)). That's probably just as unreadable with parentheses—parentheses are not a clear clue that a parameter begins here, they could also just be grouping parentheses, it's the => or { following that marks something as a function.

Without parentheses, there is no way to specify an argument type, so it will only work where the argument type is inferred (so it has to be equivalent to (item) => ..., and it cannot cover all use-cases handled by parentheses). You also cannot make the function asynchronous, because the keyword would now be undelimited. No x async =>, and async => is a one parameter function with a parameter named async, not a zero parameter asynchronous function.

Would we want to allow zero-parameter functions like foo.whenComplete(=> foo()) too?
(Or would we rather reserve those for one-argument functions with an implicit parameter name, like somethings.forEach(=> print(it.name)))?

Would we want it to work for block bodies too? x.forEach(thing { print(thing.name));
Probably not, that's not readable. The => and that it's a single expression following it, makes foo => stand out in a way that foo { .... } does not. If anything, that can be mistaken for a control flow structure.

I fear that we might end up with style guides requiring or disallowing the parentheses when they are no necessary. If you change (item, thing) => ... into (item) => ..., you would suddenly also need to remove the parentheses. That's annoying.
On the other hand, I know that some groups of programmers will definitely make one of the choices and require everybody to do the same thing, rather than allowing two different styles ("coding style" hard-liners who believe that you shouldn't need to waste time making a choice in such cases).

@lrhn lrhn added the feature Proposed language feature that solves one or more problems label Apr 29, 2019
@munificent
Copy link
Member

Would we want to allow zero-parameter functions like foo.whenComplete(=> foo()) too?
(Or would we rather reserve those for one-argument functions with an implicit parameter name, like somethings.forEach(=> print(it.name)))?

I think we should reserve it for the latter.

@eernstg
Copy link
Member

eernstg commented May 1, 2019

This is related to #265 which is about eliminating the argument entirely when there is exactly one required argument.

We could then write somethings.forEach(=> print(name)) where the argument is named this and is accessed implicitly for invocation of the getter name. An alternative design would name the argument it (like in Kotlin) and yield somethings.forEach(=> print(it.name)).

Zero-argument functions would still have to be written explicitly, but that's already a bit shorter than a one-argument function (so the motivation for abbreviating it is less compelling).

@munificent
Copy link
Member

We could then write somethings.forEach(=> print(name)) where the argument is named this and is accessed implicitly for invocation of the getter name.

I think that's probably the wrong default. Many closures want to access the original surrounding method's this in the body, so shadowing that with the implicit lambda parameter may be both confusing and not what they want. Code like this is common:

somethings.forEach((foo) => methodOnThis(foo, 3));

It would be nice to be able to shorten it to, say:

somethings.forEach(=> methodOnThis(it, 3));

@eernstg
Copy link
Member

eernstg commented May 8, 2019

[Edit May 28th 2019: A custom binding of this is such a deep change to the semantics of code that, at this point, I strongly prefer to have it only when there is a local syntactic flag for it. So I changed #265 to use the parameter name it. Adjusted the text below accordingly.]

@munificent wrote:

Many closures want to access the original surrounding method's this in the body

Right, the updated proposal #265 only supports the form that you prefer:

somethings.forEach(=> methodOnThis(it, 3));

@deadsoul44
Copy link

Can we go one step further and drop that ugly arrow also? If it has no left-hand side, it does not make sense to keep it.

@lrhn
Copy link
Member

lrhn commented Sep 15, 2023

If we drop the arrow, what's left is just the expression. How then would we tell that it is intended as the body of a function, and not as an expression which evaluates to a function?

@mateusfccp
Copy link
Contributor

If we drop the arrow, what's left is just the expression. How then would we tell that it is intended as the body of a function, and not as an expression which evaluates to a function?

If it becomes a reserved keyword could its presence be used to disambiguate the expression?

@lrhn
Copy link
Member

lrhn commented Sep 15, 2023

Probably not enough. There needs to be some delimiter.

T id<T>(T x) =>x;
List<int Function(int)> fns = ...;
... fns.map(id)... // means what?

If we can omit the arrow and variable name, the syntax can no longer distinguish between the meanings that are currently written:
fns.map(id) and fns.map((_) => id).

Both are type-valid today.

@mateusfccp
Copy link
Contributor

mateusfccp commented Sep 15, 2023

Probably not enough. There needs to be some delimiter.

T id<T>(T x) =>x;
List<int Function(int)> fns = ...;
... fns.map(id)... // means what?

If we can omit the arrow and variable name, the syntax can no longer distinguish between the meanings that are currently written: fns.map(id) and fns.map((_) => id).

Both are type-valid today.

I'm not sure I understand the problem...

id does not include it, so it would be simply interpreted as it is today. Basically:

  • any expression that does not include it is interpreted as it is today
  • any expression that includes it is interpreted as a single-parameter function. As it is a reserved keyword, there won't ever be any function/method/variable it, so it's not ambiguous
T id<T>(T x) => x;
T power<T>(T x) => x * x;
List<int Function(int)> fns = ...;
... fns.map(id)... // Means the same as today
... fns.map(power)... // Means the same as today
... fns.map(it)... // The `it` expressions means `(it) => it`, and is unambiguous
... fns.map(it * it)... // As `it` is used, this means `(it) => it * it`

@lrhn
Copy link
Member

lrhn commented Sep 15, 2023

How deep can it contain it? How do you know where to stop?

Take:

var x = foos.addAll(bar.map(it+1));

Is this

var x = foos.addAll((it) => bar.map(it+1));

or

var x = foos.addAll(bar.map((it) => it+1));

Or, heck,

var x = (it) => foos.addAll(bar.map(it+1));

It looks "easy" here because I use recognizable names that you might think you know the meaning of, but compilers don't recognize names as having meaning. It just looks at syntax.
And it can't use the types because the type inference depends on the syntactic structure, so how it's parsed cannot depend on types. Also, I bet I can come up with code that can be type-valid either way it's parsed.

If we say "innermost possible expression", then that's ... always (it) => it. So, innermost argument expression, but then.

radixes.forEach(print(number.toString(radix:it)));

I guess "innermost argument expression which isn't it itself", but then this still becomes

radixes.forEach(print((it) => number.toString(radix:it)));

which doesn't work.

One can then say "can't use shorthand for that, have to be explicit", and that'll probably work, but also make refactoring harder.

Say you start with

extension on String {
  void printToLog() {
    log.print(this);
  }
}
/// ...
foos.forEach(it.toString().printToLog());

Then you inline the .printToLog() (don't know if you can do that for in general, but it would work here) and you get:

foos.forEach(log.print(it.toString()));

... which does not work, because it.toString() is now in argument position, not receiver.

I think it will be too chaotic without some textual delimiter saying where a new function body starts, because without that, everything looks like expressions, and expressions can occur anywhere already.

@mateusfccp
Copy link
Contributor

mateusfccp commented Sep 15, 2023

Yeah, not that I prefer omitting completely the arrow. Actually, I usually prefer more explicit approaches, I was just brainstorming.

I didn't think about nesting, and although it is indeed solvable I think it would actually generate more confusion, as there would be a lot of "subrules" that the user would have to know in order do properly use it. Not knowing it could even raise silent undesired behaviors.

I agree that the arrow is ugly, tho, haha

@Azbesciak
Copy link

Probably not enough. There needs to be some delimiter.

T id<T>(T x) =>x;
List<int Function(int)> fns = ...;
... fns.map(id)... // means what?

If we can omit the arrow and variable name, the syntax can no longer distinguish between the meanings that are currently written: fns.map(id) and fns.map((_) => id).

Both are type-valid today.

One mayor difference. in the first case fns.map(id) you are passing the reference to the function. Or at east it looks like this for other languages users. In the second you crete an in-place function/lambda or something.

In kotlin you have, for the first concern, {} functions, so if you would like everything to the id they you should pass fns.map { id }, otherwise it must be the reference to the function.

Do not complicate things. If you want to change the syntax to that form, those {} are necessary IMHO. A similar thing is in scala, but it is a bit different story, not to use anymore I thing (single time use of _ ...).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems small-feature A small feature which is relatively cheap to implement.
Projects
None yet
Development

No branches or pull requests

7 participants