Closed littledan closed 5 years ago
There are two issues with this:
(a) => a.x(y)
or (a) => x.call(a, y)
. If you mean the former, then writing obj.x(y)
is more straightforward. If you mean the latter, then this is supported by writing x.call(?, y)
.x
comes after a .
; how could it refer to a variable rather than a property?Leaving ?
as a token with no other adornments or syntax keeps the concept simple to understand. If we allowed ?.x
there could be confusion as to the meaning of f(?.x(y))
for new users: Is it a => f(a.x(y))
or f(a => a.x(y))
. While we could be definitive about the meaning, it's still an adoption hazard as the intuition between different individuals may vary.
Well, I think the scope question is a big risk of this proposal regardless. Some may expect f(g(?))
to translate to _ => f(g(_))
. I'm not sure what the solution to this problem is.
I strongly believe f(g(?))
should always be f(_ => g(_))
. Anything else we choose could seem like arbitrary behavior that is difficult to reason over. The simplest and most consistent approach is to bound ?
within an argument list similar to the spread syntax (excluding array spread, of course).
This does mean that if you want _ => f(g(_))
you have to write _ => f(g(_))
. Perhaps some type of function composition operator (either ∘
or something else) is in order for f ∘ g
, then you could have (f ∘ g)(?)
. Alternatively, you can define const compose = (...funcs) => funcs.reduce((f, g) => _ => f(g(_)));
and write compose(f, g)(?)
. Maybe a Function.compose
static method?
~When I was thinking about pipeline, I had considered a parenthesized pipeline head form, like:~
(a, b) |> f(?, ?)
~Which, in turn, would evaluate (approximately) like this:~
((x, y) => f(x, y))(a, b)
~With a parenthesized head, we could do something like:~
(?) |> g |> f // x => x |> g |> f
// x => f(g(x)) - simplified (aside from evaluation order)
~With backpipe (<|
), this would be:~
f <| g <| (?) // x => f <| g <| x
// x => f(g(x)) - simplified
On second thought, let's not go there... its a silly place...
I strongly believe
f(g(?))
should always bef(_ => g(_))
.
I agree 100%. I was just trying to say, if we create user expectations that some things surrounding the call are put into the desugared arrow function, we run the risk of making that intuition go too far. A simple rule might be, "the things in the parentheses are what's evaluated multiple times".
So if I understand correctly, you expect the semantics to be:
f(?)
,
f
is fixed and immutable with respect to the resulting partially applied function.undefined
is fixed and immutable with respect to the resulting partially applied function.o.f(?)
,
o.f
is fixed and immutable with respect to the resulting partially applied function.o
is fixed and immutable with respect to the resulting partially applied function.For the purposes of relating these runtime semantics to existing javascript, the following syntactic conversions share the same semantics:
Given the partially applied function f(?)
:
const g = f(?);
is roughly identical in its behavior to:
const g = (f => x => f(x))(f);
Given the partially applied function o.f(?)
:
const g = o.f(?);
is roughly identical in its behavior to:
const g = (o => (f => x => f.call(o, x))(o.f))(o);
Would that be a fair assessment?
A simple rule might be, "the things in the parentheses are what's evaluated multiple times".
After discussing this with some others on the TypeScript team it seems it would be better if the argument list were to be eagerly evaluated as well.
@rbuckton Sounds like a reasonable option to me. The hazard here would be if you have something like f(?, [])
where f
expects to be able to modify the second argument as a fresh value. This is one reason why, in default arguments and field initializers, it's evaluated each time rather than once.
However, I think this is pretty unlikely for this sort of case, since we're on the other end of the function call boundary.
By the way, about the hazards of allowing a form like this: I believe in the presentation to TC39, you said that the choice of ?
could be up for discussion. If we switched to something that wasn't under consideration for the optional chaining operator, such as &
, I believe adding ?.method(arg)
would cause less of a grammar issue. To avoid an ASI hazard, though, we might have to prohibit &.
at the beginning of a StatementExpression. But I think this is pretty unlikely anyway.
Note that this case would be useful for some pipeline operator cases where function calls and method calls are interspersed, discussed further in https://github.com/tc39/proposal-pipeline-operator/issues/13 .
@rbuckton Sounds like a reasonable option to me. The hazard here would be if you have something like
f(?, [])
wheref
expects to be able to modify the second argument as a fresh value. This is one reason why, in default arguments and field initializers, it's evaluated each time rather than once.
We always have an arrow function as our "escape hatch" for cases like this. If you need the array to be fresh every time, you can write (x => f(x, []))
instead of f(?, [])
. Eager evaluation is easier to explain and aligns well with the behavior of |>
.
By the way, about the hazards of allowing a form like this: I believe in the presentation to TC39, you said that the choice of
?
could be up for discussion. If we switched to something that wasn't under consideration for the optional chaining operator, such as&
, I believe adding?.method(arg)
would cause less of a grammar issue.
This may be less of an issue now that optional-chaining is considering using ??.
as the chaining token as there would no longer be a grammar ambiguity.
Would ???[?]
be an object and property placeholder optionally chained?
No, x[?]
wouldn't be legal in any case. This proposal only allows for ?
to appear immediately within an argument list (not inside of element access). If we allowed ?
anywhere else it could only be part of a call. If we allowed ?
in the receiver position (which I'm still not certain is a good idea), we might permit ???[x](?)
. If we allowed this, I'd be tempted to only allow these forms:
?.x()
as a shorthand for a => a.x()
?[x]()
as a shorthand for a => a[x]()
?()
as a shorthand for a => a()
The problem is that 2 and 3 above are ASI hazards:
// 2
foo()
?[x]()
:label
// 3
foo()
?(a)
:label
Although, you're not as likely to run into these hazards as a partial application on its own is not very useful.
However, I'm much more likely stay away from binding the receiver with ?
as it can still be done via an arrow (e.g. (a => a??[x]())
) instead.
Closing in favor of further discussion in #23.
I was expecting we'd have a form like
Was this considered? What was the reason for not having it?