Open eernstg opened 1 week ago
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.
var xs2 = [1, 2, 3].map(#.toString());
There are no limitations there on what kind of expression you can use.
@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:
https://github.com/dart-lang/language/issues/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.
@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
.
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'.
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.
(Another point: while designing the feature, we'd better avoid referring to meaningless names like bar and baz; normally, the method name tells us whether the method expects a closure as a parameter (onError, onClick, etc). Otherwise, the comparison is unfair: in OP, you use map
, which everybody knows what it is, but in the discussion of #
, you use bar and baz)
I prefer the other proposals, because
['a', 'b', 'c'].map((it) => Text(it, style: DefaultTextStyle());
with this proposal.[1, 2, 3].map(Text(.toString()))
apparently doesn't work too?[1, 2, 3].map(.toString());
was changed to [1, 2, 3].map(it.toString());
to address 2.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
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.
I'm biased of course, but I'd agree that https://github.com/dart-lang/language/issues/3786 should be accepted as well, which does this directly with T.foo
. It's not necessarily shorter but it does allow you to be explicit and not rely on type inference.
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 typeR 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: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
andforEach
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:
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
whereid
is an identifier andsj
is derived from<selector>
, forj
in1 .. k
, ande
is not a subexpression of an expression of the form.id s1 .. sk .. sn
wheresj
is derived from<selector>
forj
in1 .. n
(that is,e
already contains all the selectors).If the context type of
e
is of the formR Function(T)
for some typesR
andT
thene
is implicitly transformed into((T x) => x.e s1 .. sk)
, wherex
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 ofR Function(T)
becauseT
isn't an interface type that declares a member namedfoo
, and there is no extension method namedfoo
which is applicable to a receiver of typeT
; orx.foo
is an expression that has no errors whenx
has typeT
, but that type is not a subtype ofR
, and hence the function literal as a whole isn't assignable toR Function(T)
.