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

Consider adding placeholder for receiver binding #23

Closed
rbuckton opened this issue Feb 9, 2018 · 22 comments
Closed

Consider adding placeholder for receiver binding #23

rbuckton opened this issue Feb 9, 2018 · 22 comments

Comments

@rbuckton
Copy link
Collaborator

rbuckton commented Feb 9, 2018

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
Copy link
Member

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
Copy link
Collaborator Author

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
Copy link
Member

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
Copy link
Collaborator Author

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
Copy link
Collaborator Author

It could also be something as simple as ?value.

@rbuckton
Copy link
Collaborator Author

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
Copy link
Member

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
Copy link
Collaborator Author

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)

@rbuckton rbuckton changed the title Consider adding a ?this placeholder Consider adding a (?) placeholder for receiver binding Apr 2, 2019
@rbuckton rbuckton changed the title Consider adding a (?) placeholder for receiver binding Consider adding placeholder for receiver binding Apr 2, 2019
@phaux
Copy link

phaux commented Mar 26, 2020

@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
Copy link

phaux commented Mar 26, 2020

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
Copy link
Member

ljharb commented Mar 26, 2020

@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
Copy link
Collaborator Author

.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
Copy link
Member

ljharb commented Mar 26, 2020

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

@phaux
Copy link

phaux commented Mar 26, 2020

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
Copy link

phaux commented Mar 26, 2020

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
Copy link

Fenzland commented Jun 4, 2020

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

@rbuckton
Copy link
Collaborator Author

rbuckton commented Oct 14, 2021

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:

  • Partial application is eager evaluation (just like Function.prototype.bind). All non-placeholder arguments are eagerly evaluated and fixed at the time the partial application expression is evaluated.
  • Placeholders may only appear as a full argument, and may not be used within an expression in an argument.

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
Copy link
Member

ljharb commented Oct 14, 2021

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

@rbuckton
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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
Copy link
Member

ljharb commented Oct 14, 2021

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

@rbuckton
Copy link
Collaborator Author

rbuckton commented Oct 14, 2021

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants