tc39 / proposal-partial-application

Proposal to add partial application to ECMAScript
https://tc39.es/proposal-partial-application/
BSD 3-Clause "New" or "Revised" License
1.02k stars 25 forks source link

Consider adding placeholder for receiver binding #23

Closed rbuckton closed 3 years ago

rbuckton commented 6 years ago

This proposal extends CallExpression to allow ?this.prop() or ?this[expr]() as a way to define a placeholder argument for a partially applied CallMemberExpression. Both ?this and ? could be combined in the same expression, in which case the ?this placeholder argument will be the first argument in the partially applied function result.

Syntax

const arrayToStringArray = ?this.map(x => "" + x);

Grammar

PartialThisProperty[Yield, Await] :
  `?this` `[` Expression[+In, ?Yield, ?Await] `]`
  `?this` `.` IdentifierName

CallExpression[Yield, Await] :
  ...
  PartialThisProperty[?Yield, ?Await] Arguments[?Yield, ?Await]

ExpressionStatement[Yield, Await] :
  [lookahead ∉ { `{`, `function`, `async` [no LineTerminator here] `function`, 
    `class`, `let [`, `?` }] Expression[+In, ?Yield, ?Await];

Examples

// valid
const a = ?this.f();
const b = ?this.f(?);
const c = ?this[expr]();
const d = ?this[expr](?);

// syntax errors
?this.f(); // `?` disallowed at start of ExpressionStatement
const e = ?this;
const f = ?this.f;
const g = ?this[expr];
const h = ?this[?](); // though we could consider allowing this in the future

The following show approximate syntactic transformations that emulate the proposed semantics:

const a = ?this.f();
// equiv:
const a = _this => _this.f();

const a = ?this.f(?);
// equiv:
const a = (_this, _0) => _this.f(_0);

const a = ?this.f(g());
// equiv:
const a = ($$temp1 = g(), _this => _this.f($$temp1));

const a = ?this[g()]();
// equiv:
const a = ($$temp1 = g(), _this => _this[$$temp1]());

const a = ?this[g()](?);
// equiv:
const a = ($$temp1 = g(), (_this, _0) => _this[$$temp1](_0));

const a = ?this[g()](h());
// equiv:
const a = ($$temp1 = g(), $$temp2 = h(), _this => _this[$$temp1]($$temp2));

const a = ?this.f(?this.g());
// equiv:
const a = ($$temp1 = _this => _this.g(), _this => _this.f($$temp1));

It is not possible to reference ?this on its own in an argument list.

Alignment with the pipeline operator (|>)

When combined with the pipeline operator proposal, this feature could allow the following (based on original example in https://github.com/tc39/proposal-pipeline-operator/wiki):

anArray 
  |> pickEveryN(?, 2)
  |> ?this.filter(f)
  |> makeQuery
  |> readDB(?, config)
  |> await
  |> extractRemoteUrl
  |> fetch
  |> await
  |> parse
  |> console.log(?);

Alignment with other proposals

This syntax aligns with the possible future syntax for positional placeholders (e.g. ?0, ?1) as proposed in #5. If both proposals are adopted, then when using ?this all positional placeholders are offset by 1 with respect to their actual ordinal position in the argument list (similar to Function.prototype.call). For example:

const f = ?this.f(?0, ?1);
// equiv:
const f = (_this, _0, _1) => _this.f(_0, _1);
littledan commented 6 years ago

Will it be OK to use a token containing this for a construct which is not referencing the current lexical this value?

If optional chaining is switching to ??. syntax, why not just use ?. for this case?

rbuckton commented 6 years ago

I'd rather stay away from ?. for this case as its too visually similar to ??. and has the same meaning in other languages.

As far as using this, I could use any identifier or keyword, even ?$ or ?_, but I feel that ?this is clearer as to its purpose and is much more descriptive.

littledan commented 6 years ago

I don't understand the similarity concern. I could see avoiding ? as it is similar to ??, but the idea of the ??. proposal is that ?? is considered a token by itself, which can compose with ., [ and (. If partial application sticks with using ?, it should be because ? is a distinct enough token from ?? that it is not confusing.

I agree that using another token like ?$ or ?_ would be more confusing.

rbuckton commented 6 years ago

Besides, I'm likely to change the grammar above to include a single-step optional chain as well, e.g.:

?this??.prop()
?this??[expr]()

Yeah, that's a lot of ?s there, but its arguably better than ???.prop(), etc. Things improve as well if syntax highlighters are updated to treat ?this as a single token as a keyword.

rbuckton commented 6 years ago

It could also be something as simple as ?value.

rbuckton commented 6 years ago

Although, I'm less likely to consider adding support for optional chaining into this proposal if we eventually have a syntax for optional pipelines (e.g. ?>).

littledan commented 6 years ago

Another option would be to change the placeholder to not be ? so that there is no ambiguity with ??. For example, if the placeholder is ^^, then ^^??.f() might not be as confusing. It's a bit of ASCII soup, but that's sort of inherent in these two proposals, as they are all about adding tokens as shorthand.

rbuckton commented 6 years ago

I don't particularly like ^^ as a token for this purpose, it feels arbitrary in this context compared to ? which could be visually interpreted as a placeholder for something.

I suppose that if optional chaining is using ??., ??[ and ??( for chaining in an infix position, then we could leverage ?., ?[ and ?( for partial application since it's in a prefix position. In all of those cases the usage would have restrictions that it's only valid as part of a call, so ?.x and ?[x] would be invalid, but ?.x() and ?[x]() would be valid (and possibly even ?()). With a few tweaks, the following could all have a valid meaning:

// partially apply property access call
?.x()         // _a => _a.x()
?.x(?)        // (_a, _0) => _a.x(_0)

// partially apply element access call
?[expr]()     // $$temp = expr, _a => _a[$$temp]()
?[expr](?)    // $$temp = expr, (_a, _0) => _a[$$temp](_0)
?[?]()        // (_a, _b) => _a[_b]()
?[?](?)       // (_a, _b, _0) => _a[_b](_0)
a[?]()        // $$temp = a, _b => $$temp[_b]()
a[?](?)       // $$temp = a, (_b, _0) => $$temp[_b](_0)

// partially apply function call
?()           // _a => _a()
?(?)          // (_a, _0 => _a(_0)
phaux commented 4 years ago

@rbuckton If it was to be implemented then I would expect the following syntaxes to also work or at least be considered in the future (using # as the placeholder to make it more clear):

#.prop === (x) => x.prop // property access
#?.prop === (x) => x?.prop // optional property access
#(arg) === (x) => x(arg) // call
#?.(arg) === (x) => x?.(arg) // optional call
#.method() === (x) => x.method() // method call (this proposal)
#?.method() === (x) => x?.method() // optional method call
#.method?.() === (x) => x.method?.() // method with optional call
// etc

So using ? as the placeholder would result in ?.prop vs ??.prop confusion.

phaux commented 4 years ago

I don't know if it's feasible, but another option is to not have any symbol at all:

const friendsList =
  getData('users')
  |> JSON.parse
  |> .filter(.friends.includes(currentUser))
  |> .map(.name)
  |> unique
  |> .join(', ')

// same as

const friendList = 
  getData('users')
  |> JSON.parse
  |> users => users.filter(user => user.friends.includes(currentUser))
  |> users => users.map(user => user.name)
  |> unique
  |> users => users.join(', ')
ljharb commented 4 years ago

@phaux that wouldn't be feasible, because dot access and bracket access must both work, and [Symbol.iterator], ['abc'], etc, all look like they're an array. Also, .1.toString is a function value already.

rbuckton commented 4 years ago

.1 wouldn't be an issue since 1 isn't a valid IdentifierStart. Prefix-dot is still an option for element access as .[x] given the precedent set by o?.[x].

ljharb commented 4 years ago

@rbuckton you can't put an expression in the pipeline?

phaux commented 4 years ago

According to latest node.js:

> [].map(.1)
Thrown:
TypeError: 0.1 is not a function
    at Array.map (<anonymous>)

but

> [].map(.a)
Thrown:
[].map(.a)
       ^

SyntaxError: Unexpected token '.'

so it could work as long as the property names aren't numeric.

phaux commented 4 years ago

and for computed property names it would be .[x] which is consistent with optional chaining which also requires an extra dot.

> [].map(.["x"])
Thrown:
[].map(.["x"])
       ^

SyntaxError: Unexpected token '.'

I'm not entirely sure if it's worth treating dot+numbers versus dot+identifier so much differently. I'm just throwing out ideas.

Fenzland commented 4 years ago

leading . or leading ? will let who not use semicolon make more mistake, and make compiler implement more difficult.

rbuckton commented 3 years ago

Given the syntax and semantics introduced in #49, and the advancement of Hack-style pipelines to Stage 2, I no longer believe introducing a placeholder for the receiver binding is an avenue to pursue.

This suggestion was initially driven by a demand to support piping a callee in F#-style pipelines:

[1, 2, 3] |> ?.map(x => x + 1);

However, Hack-style pipelines do not require this capability, as [1, 2, 3] |> ^.map(x => x + 1) does not involve partial application.

In #49, we introduced the prefix token ~ to indicate partial application to satisfy a constraint to avoid "the garden path". This token is inserted between the callee and the argument list to make the following semantics very explicit:

Both the smart-mix and Hack-style pipeline proposals were considering a prefix token like +> that came before the callee:

+> f(?)

However, in both cases these expressions are lazily evaluated and semantically the same as x => f(x). While this would allow for partial expressions (i.e., +> ? + ?), arrow functions are already a perfectly viable (and more flexible) solution for lazy evaluation.

A prefix token that comes before the callee is not conducive to eager evaluation, as it can introduce confusion as to which part of a more complex expression is to be partially applied:

+> o.f().g(?) 

To suit eager evaluation, the above would necessarily be a syntax error since the first call expression we encounter is o.f(), which leaves a dangling .g(?) that is not partially applied.

We chose ~( as it mitigates this confusion. It becomes very clear which argument list is partially applied:

o.f().g~(?)

This change also means that only arguments can be partially applied, since the prefix applies to the argument list and not the callee. Since the callee is not an argument, it cannot be partially applied. Instead, you have two alternatives if you need to "partially apply" the callee: arrow functions and utility functions.

As mentioned above, arrow functions are lazily evaluated. They introduce a closure over the outer environment record, which means that they will observe state changes to closed-over variable bindings. In addition, the body of an arrow function is repeatedly evaluated each time it is called, meaning that any side effects within the body of the arrow can be observed. If your code is structured in such a way that there are no mutations to closed over variables and the arrow body does not contain side effects, then an arrow function is a perfectly acceptable solution to providing a partially-applied callee.

If eager-evaluation semantics are still necessary, however, its fairly easy to write utility functions that can support partial application of a callee:

const call = (callee, ...args) => callee(...args);
const invoke = (receiver, key, ...args) => receiver[key](...args);

const callWith1And2 = call~(?, 1, 2);
const invokeSayHello = invoke~(?, "sayHello");
ljharb commented 3 years ago

So in a Hack pipeline, ^o.f~(a, ?, c) would not eagerly cache o as the receiver for the call?

rbuckton commented 3 years ago

If eager-evaluation semantics are still necessary, however, its fairly easy to write utility functions that can support partial application of a callee:

If such helpers seem useful, especially with respect to partial application, I suggest you file an issue on https://github.com/js-choi/proposal-function-helpers for their inclusion (although call would need a different name..)

rbuckton commented 3 years ago

So in a Hack pipeline, ^o.f~(a, ?, c) would not eagerly cache o as the receiver for the call?

Did you mean ^.f~(a, ?, c)? If so, the result of that expression would be a partially applied function whose receiver is whatever ^ was at the time the pipeline is evaluated, and whose callee is whatever ^.f was at the time it was evaluated.

Pipelines are still evaluated left-to-right, so in source |> ^.f~(), the ^.f~() portion won't be evaluated until after source and will therefore have a valid expression on which to operate.

ljharb commented 3 years ago

oops, yes, i did mean ^.f - and great, thank you.

rbuckton commented 3 years ago

For example:

const bob = {
  name: "Bob",
  sayHelloTo(name) {
    console.log(`Hello ${name}, I'm ${this.name}!`);
  }
};

const sayHello = bob |> ^.sayHelloTo~(?);

sayHello("Alice"); // prints: Hello Alice, I'm Bob!