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

Method-to-function conversion #4150

Open
eernstg opened this issue Nov 4, 2024 · 6 comments
Open

Method-to-function conversion #4150

eernstg opened this issue Nov 4, 2024 · 6 comments
Labels
brevity A feature whose purpose is to enable concise syntax, typically expressible already in a longer form feature Proposed language feature that solves one or more problems

Comments

@eernstg
Copy link
Member

eernstg commented Nov 4, 2024

Discussions in #357 strongly suggest that we will support a primary expression of the form '.' <identifier> (e.g., .foo). The semantics of such terms will most likely be determined by the context type.

This issue is a proposal that we should generalize the semantics of this syntax to cover an additional case: If .foo is used in a context where an expression of type R Function(T) is expected then it is implicitly transformed into (T x) => x.foo. Similar conversions are applied when a chain of selectors are present, e.g., .foo(1)?.whereType<int>()[4] would become (T x) => x.foo(1)?.whereType<int>()[4]. For example:

void main() {
  // Proposed form.
  var xs1 = [1, 2, 3].map(.toString());

  // Same thing, written without this feature.
  var xs2 = [1, 2, 3].map((int x) => x.toString());
}

This feature will only enable a small amount of abbreviation, but it is likely to be convenient to have in a large number of invocations of map and forEach on collections, as well as many other situations involving callback arguments, and this would justify having the feature even though it doesn't do a lot each time it is used.

Proposal

Syntax

The grammar is adjusted as follows:

<primary> ::= // Add one alternative at the end.
    :    ...
    |    '.' <identifier>

Several other proposals are expected to introduce this new kind of expression. If and when we get any of those proposed features then this grammar change is not needed.

Static analysis

Assume that e is an expression of the form .id s1 .. sk where id is an identifier and sj is derived from <selector>, for j in 1 .. k, and e is not a subexpression of an expression of the form .id s1 .. sk .. sn where sj is derived from <selector> for j in 1 .. n (that is, e already contains all the selectors).

If the context type of e is of the form R Function(T) for some types R and T then e is implicitly transformed into ((T x) => x.e s1 .. sk), where x is a fresh name.

If the resulting function literal is or contains a compile-time error then it should be reported in terms of a failure to perform the method-to-function conversion, plus information about the error that arises after the conversion. For example, .foo may be an error in the context of R Function(T) because T isn't an interface type that declares a member named foo, and there is no extension method named foo which is applicable to a receiver of type T; or x.foo is an expression that has no errors when x has type T, but that type is not a subtype of R, and hence the function literal as a whole isn't assignable to R Function(T).

@eernstg eernstg added feature Proposed language feature that solves one or more problems brevity A feature whose purpose is to enable concise syntax, typically expressible already in a longer form labels Nov 4, 2024
@lrhn
Copy link
Member

lrhn commented Nov 4, 2024

There would be no feature ambiguity since function types don't have static namespaces.

I'm worried that using the same syntax for different things can lead to confusion.
It's a nice syntax. If you could have only one of these, which one would you choose?

Why stop at one level?
If the context type is R Function () Function(X) could we allow .foo to be (X x)=>()=>x.foo?
(Maybe that is why we stop at one level!)

This is a more indirect approach to a feature that would turn T.foo into (T value) => value.foo when foo is an instance member of T. This feature does that and allows you to omit the T.
Maybe we shouldn't do that if we don't also allow the explicit T.foo.
But if we allow that, and also allow static and instance members with the same name, then .foo can denote both an instance member and a static member, and the only thing distinguishing them is the context type, which must be a function type for the instance member and T for the static member.
Maybe that's a little too subtle.

@tatumizer
Copy link

#8 looks much more general and more readable IMO:

var xs2 = [1, 2, 3].map(#.toString());

There are no limitations there on what kind of expression you can use.

@eernstg
Copy link
Member Author

eernstg commented Nov 7, 2024

@lrhn wrote:

Maybe we shouldn't do that if we don't also allow the explicit T.foo.

Good points! For T.foo, I think it could just coexist with .foo in context R Function(T). T.foo is useful when there is no context type. However, in a case like OverdueReminderFormatter.format(someValue) it does seem more convenient to be able to say .format(someValue) if we have R Function(OverdueReminderFormatter) as the context type.

@tatumizer wrote:

#8 looks much more general and more readable IMO:

True, with the ability to denote the actual argument in the expression we do have much more expressive power.

var xs1 = [1, 2, 3].map(.toString()); // Based on the context type schema `_ Function(int)`.
var xs2 = [1, 2, 3].map(#.toString()); // A concise function literal based on a proposal in #8.

The primary difficulty is that there is no firm syntactic basis for delimiting the function literal as a whole. (I said already in the original posting of issue #8 that 'The main issue with this approach is that it is ambiguous', so there's nothing new about that).

For example:

foo(bar(baz(#.toString()))) // Could mean
foo(bar(baz(((x) => x).toString()))) // or
foo(bar(baz((x) => x.toString()))) // or
foo(bar((x) => baz(x.toString()))) // or
foo((x) => bar(baz(x.toString()))) // or
(x) => foo(bar(baz(x.toString())))

We'd also need to have disambiguation rules for the case where there is more than one occurrence of #, and we could have a single function literal on our hand where the actual argument is used multiple times, or we could have several function literals using their argument just once, or some mixture.

I don't think we can allow this kind of decision to be based on the static types of the expressions involved (because we don't even know the structure of those expressions, and different structures may have subexpressions with completely different static types). In other words, we must decide on the structure based on the syntax alone. I think this implies that the proposal that uses # must be extended with more syntax in order to delineate the function without ambiguity.

Method-to-function conversion is much simpler in this respect: It includes the selector chain after .identifier, and that's it.

Also, all the heated debates about .identifier based constructs (#357, in particular) makes it very, very likely that we will have this syntax. This means that the method-to-function conversion is just one more case where we can use the context type to perform a small transformation on an .identifier based expression. And we may well generalize the syntax with further additional cases.

@tatumizer
Copy link

@eernstg wrote:

I don't think we can allow this kind of decision to be based on the static types of the expressions involved

I don't understand that. The whole idea of the original proposal is to base the decision on the static type of the expression:

This issue is a proposal that we should generalize the semantics of this syntax to cover an additional case: If .foo is used in a context where an expression of type R Function(T) is expected then...

Without this logic, you won't be able to distinguish between .id as a shortcut to ContectType.id or the same as a shortcut to (it)=>it.id.

@eernstg
Copy link
Member Author

eernstg commented Nov 7, 2024

The whole idea of the original proposal is to base the decision on the static type of the expression:

Exactly, this proposal is syntactically unambiguous, and it uses the context type. No problem.

The #.toString() proposal is syntactically ambiguous, and hence, for that proposal:

I don't think we can allow this kind of decision to be based on the static types of the expressions involved

where 'this kind of decision' means 'delineating the function literal syntactically'.

@tatumizer
Copy link

The ambiguity you pointed to arises only in the context of nested calls like bar(baz(#.toString())).
One way of disambiguation is via static types: does bar have a parameter of function type? does baz have such a parameter?
An alternative is to flag ambiguous call and require a full syntax instead. Such instances will account for only 0.3% of all use cases (the number is made-up, as usual). That's not the reason for disqualifying an idea (shortcuts are always optimized for the most common case).

Your proposal is entirely based on the assumption that the expression starts with the dot (that's how shortcuts are currently defined). For function literals, this is too restrictive. You can't even write [1, 2, 3].map(# + 1) in a proposed syntax.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
brevity A feature whose purpose is to enable concise syntax, typically expressible already in a longer form feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

3 participants