tc39 / proposal-call-this

A proposal for a simple call-this operator in JavaScript.
https://tc39.es/proposal-call-this/
MIT License
121 stars 5 forks source link

Bikeshedding syntax #10

Open js-choi opened 3 years ago

js-choi commented 3 years ago

2022-03 plenary bikeshedding slides

Possible criteria

Syntactic clarity

Would human readers often have difficulty with determining the syntax’s grouping?

Conciseness

Is the syntax significantly improve conciseness over the status quo?

Natural word order

Is the syntax’s word order more natural (e.g., subject.verb(object)) than the status quo?

Confusability with other JS features

Is there a risk of beginners and other developers confusing the syntax with regular dot property access?

Confusability with other languages

Is there a risk of developers confusing the syntax with visually similar syntaxes from other languages – especially if they have different semantics?

Overlap with other JavaScript features

Does the syntax greatly overlap with other features of the language? (Note: A finding from the January post-plenary overflow meeting says, “In general, some overlap is okay, but too much is bad; we have to decide this on a case-by-case basis.”)

List of candidates

Receiver-first style (loose unbracketed)

This style was originally called “bind-this”, but we dropped function binding from it in 2022-03, so we renamed the style to “receiver first”.

rec :> fn(arg0)
rec ~> fn(arg0)
rec !> fn(arg0)
rec -> fn(arg0) 
rec #> fn(arg0)
rec ~~ fn(arg0)

Receiver-first style (tight bracketed)

This style was originally called “bind-this”, but we dropped function binding from it in 2022-03, so we renamed the style to “receiver first”.

rec:>fn(arg0)
rec~>fn(arg0)
rec->fn(arg0)
rec::fn(arg0)
rec:.fn(arg0)
rec-.fn(arg0)

Receiver-first style (bracketed)

rec~[fn](arg0)
rec![fn](arg0)
rec#[fn](arg0)

Function-first style

This style was originally called “call-this”, but we are now calling it “function first” to distinguish it from receiver-first call-this. See the original explainer by @tabatkins.

fn@.(rec, arg0)

This-argument style

First proposed by @rbuckton.

fn(this: rec, arg0)

Original post @rkirsling [brought up in Matrix a few days ago](https://view.matrix.org/room/!wbACpffbfxANskIFZq:matrix.org/?anchor=$ygMgd9ofK4rr0yfiXOeJFLjXpG269-ta8UygGlb4kOg&offset=17) the reasonable concern that `->` may still be confusing to beginners with `.`. > I would be GENUINELY scared at making every beginner worry about "was it `.` that I'm supposed to write? but there's also `->`..." `->` is a charged symbol. It has precedent as “method call” in Perl and PHP, but this proposal is for an operator that simply “changes the receiver of a function”, which is related but different. I’m not a huge fan of `::`, since that reads as “namespacing” to me, but I plan to tentatively switch back from `->` to `::` before the October plenary. There’s also `~>` and `~~` as possibilities. I don’t have any better ideas for its spelling right now.
bmeck commented 3 years ago

I have slight reservations about the right hand side of the operator being a dynamic variable lookup in a syntax that doesn't look like it is. In property access and method calling in particular in order to achieve a dynamic lookup you have to enclose the expression via [] like obj[method](). I don't think this is fatal, but a bit jarring to not have the expression with a wrapping set of tokens and instead matching . in only have a preceding token.

ljharb commented 3 years ago

It is a bit atypical; it would be consistent, but unfortunate, if we were forced to only do obj->[method]().

js-choi commented 3 years ago

@bmeck: Yeah, we could use an always-circumfix binary operator, like …~[…] or …![…] or whatever.

…~[…] might be confusing with @rbuckton PFA syntax’s …~(…) (tc39/proposal-partial-application#48), but they can work together and they’re kind of analogous to one another…

obj~[fn](arg) // this-call

fn~(arg, ?) // PFA

obj~[fn]~(arg, ?) // PFA with a this-call 😄
js-choi commented 3 years ago

The more I think about …~[…], the more I like it.

I’m planning to switch the operator from …->… to …~[…] soon, barring any big objections.

ljharb commented 3 years ago

I definitely think using ~ would be confusing given PFA’s change to it.

@js-choi maybe best not to so rapidly change the syntax of a proposal - i strongly prefer a dot/non-bracket mechanism, and i wouldn’t want ~ even if it’s a bracket mechanism and if PFA weren't using it, because it doesn’t convey any meaning about what it’s doing.

js-choi commented 3 years ago

Alright. We did get strong pushback about …->… from @rkirsling, though, so I worry about pushback about it from others at the plenary. At the very least, maybe we should switch to …::… before the plenary.

ljharb commented 3 years ago

That concern seems like it would apply to any left-to-right syntax, including ::, and either way that’s worth discussing in plenary. I’m fine with switching back to :: in the meantime tho.

jridgewell commented 3 years ago

We could also embrace the pipe-ness and use :> or similar X> operator.

rkirsling commented 3 years ago

That concern seems like it would apply to any left-to-right syntax, including ::, ...

For the committee as a whole that could be true, but it's actually the specific symbol -> that I view as charged with expectation—I just really don't want "wait, is it . or ->?" to ever be a dilemma JS newcomers have to face.

ljharb commented 3 years ago

@rkirsling how is that different from ". or ::"? -> doesn't seem any more common an expectation to me than :: (it's used heavily in PHP, and less so in Ruby).

rkirsling commented 3 years ago

Hmm, I mean :: makes me think of package/module access, so while it's true that in the past I saw a code example with :: and was unable to guess what we meant by it, it was clear that it was going to be a brand-new meaning.

Anyway, I'm not specifically advocating for ::; I just wanted to explain my apprehension about ->.

ljharb commented 3 years ago

I'm confused about the apprehension, is all - are there specific languages that inform your expectation that it will be confusing?

jridgewell commented 3 years ago

C++ uses -> to mean property access of an object pointer:

// this pseudo code
Obj o = {};
o.foo; // regular access

Obj* p = &o;
p->foo; // same as `(*p).foo`, which is `o.foo`
ljharb commented 3 years ago

I suspect if we made an extensive list, literally any viable infix option will probably be used as property access in some other language.

One question that guides my thinking here is: do we think more JS devs come from C++, or from PHP or Ruby?

rkirsling commented 3 years ago

Admittedly, if it were just C/C++ then perhaps that wouldn't suffice for a strong objection, but I'm thinking of my own experience of needing to reluctantly edit a Perl script—I really wasn't expecting to encounter -> vs. . in a scripting language and needed to figure out whether the implications were the same as C or not.

Are you suggesting that other choices would cause PHP/Ruby devs to worry about normal . usage? My concern is about two operators being viewed as a pair such that existing code becomes more confusing.

ljharb commented 3 years ago

My understanding of your concern is, that based on expectations from C++ and Perl (and likely others), that a->b(c) may imply the semantics of a.b(c), which will cause confusion.

To a PHP user, a::b(c) already implies those semantics, and I do not believe that will cause confusion - they're different sigils, and the positioning doesn't necessarily imply "member access" to me.

It seems to me that while we may disagree on "it will be confusing", that the two are in the same identical bucket - iow, either they're both confusing, or neither is.

rkirsling commented 3 years ago

At any rate, I'm fine with someone calling :: confusing, I just wasn't myself bringing that point to the table. 😅

rbuckton commented 3 years ago

I suspect if we made an extensive list, literally any viable infix option will probably be used as property access in some other language.

One question that guides my thinking here is: do we think more JS devs come from C++, or from PHP or Ruby?

C# also uses -> for indirect property access through a pointer, and there's a fair amount of C# developers that also write JavaScript.

acutmore commented 3 years ago

Note that -> are => lambdas in languages like CoffeeScript, Elm, Java, Julia, Kotlin and LiveScript.

js-choi commented 2 years ago

After today’s ad-hoc meeting on dataflow proposals, I have edited this issue to focus on general syntax bikeshedding. There are three possible styles.

bergus commented 2 years ago

I dislike the fn(rec: arg1) syntax. It looks more like a type declaration (which doesn't make sense in a call) or a named argument (which JS doesn't have). I do like the idea of a marker on the zeroth argument (a "this call" syntax, comparable to spread syntax), but it should still be separated by a normal comma from the rest of the arguments. Maybe fn(@rec, arg1, arg2) or using the this keyword fn(this: rec, arg1, arg2)/fn(this=rec, arg1, arg2)?

I do like both of the other syntaxes, we just should ensure that they don't collide with the extension methods, pipeline, or decorator proposals.

ljharb commented 2 years ago

My preference of those three is definitely bind-this > call-this > this-arg; "this-arg" style feels very unjavascripty (it does feel slightly typescripty, which may be a pro to some but is a con to me)

If we can find an operator for bind-this style that doesn't evoke confusion with "dot access" for some delegates, I would be pleased.

rkirsling commented 2 years ago

I like call-this quite a bit and don't dislike the explicit-this form of this-arg; I had expressed an undue amount of concern about :: in the meeting we had just now (having evidently forgotten the discussion above), but I think my opinion really boils down to: I'd prefer having the receiver be within the parens.

theScottyJam commented 2 years ago

I do like the fn@() syntax - there was some previous discussion around a syntax like that in issue #3

But, the fn(this: rec, arg0) is a pretty intriguing idea as well. Might I propose a couple of variations on that idea, that I think looks pretty good:

Array.prototype.slice(rec as this, 2, 4)
Array.prototype.slice(this rec, 2, 4)
ljharb commented 2 years ago

@rkirsling one of the primary values imo of having syntax here is that it can restore the "normal" word order of "receiver, function, arguments". Is there no form of that you'd consider acceptable?

jridgewell commented 2 years ago

one of the primary values imo of having syntax here is that it can restore the "normal" word order of "receiver, function, arguments".

This is my primary desire with bind operator. Giving the ability to express fluent APIs without cumbersome syntax is the reason I'd much rather the current bind operator. Both calll-this and this-arg styles provide essentially no benefit over context-as-first-param style that is easily achieved with just hack pipelines. Bind operator gives us the ability to do tacit programming just like a regular method chain.

I care much less about the ability to reliably .call, given that we could easily address that with an uncurryThis. But out of call-this and this-arg styles, I think this-arg is nicer. I'd just be disappointed if that's all we got out of this proposal.

rkirsling commented 2 years ago

I'm failing to understand what is natural about that word order though.

. expresses possession. a.b(c) is saying to "call a's b with c". a.b.call(a, c) straightforwardly allows you to make the implicit-by-default first argument explicit.

Contrary to the README's claims, I have no idea how to read this out loud:

[1,2,3]::Object.prototype.toString();

Possession here is unchanged; the method to be called, of course, doesn't belong to the Array prototype. We're effectively doing |> for the receiver argument, yet we're writing it without spaces for some reason.

Now, I realize that it's actually [1,2,3]::Object.prototype.toString that's being called, and we could just stop at the binding itself. But that's not SVO word order: we can say "bind the value to the function" or "the function binds the value" but either way the value is the direct object of the verb "bind".

It's worth noting here that mathematical function notation sometimes uses a subscript for a similar purpose: e.g. logb(x). We bind b first, and indeed, could pass logb as a function instead of feeding it an x right away.

So if we want bind-this, it seems we should do one of the following:

  1. Reverse ::'s operands
  2. Make explicit analogy with pipeline by using :>

(And yeah, given the statements I'm responding to, I assume you'll hate (1). Still, I'm glad that I took a couple of hours to dig into why I've found this proposal so hard to wrap my head around.)

bathos commented 2 years ago

I'm failing to understand what is natural about that word order though.

Not that one or another grammatical order is more correct than any other, but an ordinary member call is subject.verb(object), so being able to still write subject::verb(object) makes it a lot easier to scan and understand code when there’s a mix of the two application styles. Loose analogy but I think that’s the essential issue, being able to use this pattern without adding a ton of cognitive noise. I.e. receiver-first is not objectively more natural, but it’s ubiquitous in the language already.

jridgewell commented 2 years ago

. expresses possession. a.b(c) is saying to "call a's b with c".

I'd read this as "a calls b with c". (When reading the code, you don't even know what verb is to use until you scan till the end. a.b = c is "a sets b to c").

So I don't think switching . with :: materially changes anything. a::b(c) still saying "a calls b with c", the only difference is that b is no longer a property of a but an expression.

Make explicit analogy with pipeline by using :>

I'd be happy with that. a :> b(c) still preserves the subject.verb(object) that's expected with method based fluent APIs. I don't care whether we use spaces, or the which exact operator sigil we use, just that subject is inherited from the LHS and doesn't need to be repeated on the RHS.

but an ordinary member call is subject.verb(object), so being able to still write subject::verb(object)... I.e. receiver-first is not objectively more natural, but it’s ubiquitous in the language already.

+100.

rbuckton commented 2 years ago

I do like the fn@() syntax - there was some previous discussion around a syntax like that in issue #3

But, the fn(this: rec, arg0) is a pretty intriguing idea as well. Might I propose a couple of variations on that idea, that I think looks pretty good:

Array.prototype.slice(rec as this, 2, 4)
Array.prototype.slice(this rec, 2, 4)

f(x as this) would collide with TypeScript type assertions and the this type, so I would be opposed. f(this x) might be fine.

I suggested f(this: x) since it parallels TypeScript's this parameter type syntax:

function f(this: { x: number }) {
  console.log(this.x);
}

f(); // compile time error
f.call({}); // compile time error
f.call({ x: 1 }); // ok

// with a `this:` argument...
f(this: { x: 1 }); // ok
ljharb commented 2 years ago

@rkirsling . doesn't only express possession to me; it also expresses which receiver the function will receive. :: could express just the receiver part without the possession part.

bathos commented 2 years ago

Agreed, that's even true for property access without (). The LHS is the receiver; the property "possessor" may be another object.

jridgewell commented 2 years ago

Discussing with @rkirsling in chat, I'm coming around to using :>. Foo::Bar could easily be interpreted as a namespace access (which is easy to think of as property access Foo.Bar). Having a :> operator, which I think would be more likely to be surrounded by spaces, could help with the understanding that Bar is not a property access, but something that exists in calling lexical scope.

ljharb commented 2 years ago

fwiw, while obj:>fn(a, b, c) looks like a little face smirking at me inside the middle of my function call, it does meet all of my criteria.

js-choi commented 2 years ago

I’m fine with obj:>fn(a, b, c), but is that much different than obj->fn(a, b, c) (which I’m also fine with but with which some people have expressed more discomfort)?

(Also, I encourage everyone (who hasn’t already) to read @tabatkins’ argumentation in favor of the call-this style in their original explainer. In particular, the RHS syntax of the bind-this style is slightly complex, which disappears with the call-this style…or this-arg style too.)

jridgewell commented 2 years ago

-> has the same confusion with property access semantics (it is property access in C++). Based on the chat, I think that should be avoided, even more than ::.

theScottyJam commented 2 years ago

I like the :> syntax better than ::, I feel it's more intuitive, however, if we go with a :> syntax, this will probably cause some issues with precedence. The obj::fn() syntax gave :: a very high precedence, with some special rules to ensure it got applied before a function on the RHS got called. Conceptually, one would expect :> to have a much lower precedence, one that's comparable to the pipeline operator, but I'm not sure if that's possible if we're still wanting it to apply before a function on the RHS gets called.

e.g.

obj |> 2 + f(%) // valid
obj :> 2 + f() // Invalid

Update: On second thought, this might not be much of an issue. The above example is nonsense code anyways (don't know what people would expect to happen with that if they wrote it), and flipping the operands of + would cause it to work as expected, i.e. obj :> f() + 2 would correctly "this-pipe" obj into f() then add two.

theScottyJam commented 2 years ago

Ok, I think I've come up with a couple of odd edge cases related to precedence. If we go with a :> operator, how would we handle these scenarios?

x :> await f() // Allowed?
x :> yield f() // Allowed?
x :> !f() // Allowed?
x :> ++f() // Allowed?
x :> f()++ // Allowed?
x :> (f()) // Allowed?
x :> (f() + 2) // Allowed?
x :> f(g()).h() // Allowed?
x :> namespace.f() // Allowed?

Additionally, I know @jridgewell mentioned the desire to use this as an extension proposal. This would mean, ideally, we would want the :> operator to have a precedence similar to . on both sides of the operator, if we want to be able to slip it into the middle of a fluid API calls. But, I'm not sure how possible this is. This scenario, for example, wouldn't work.

await value
  .f()
  :> g() // I can't just use ":>" here
  .h()
  .i()

// The above is the same as this
(await value.f()) :> g().h().i()
// not this (what we wanted)
await (value.f() :> g().h().i())
ljharb commented 2 years ago

I’m not concerned about the need to await the value of a function that’s then called this way - the primary use case is for imported or extracted methods, which are always synchronously available.

js-choi commented 2 years ago

After some discussion with other TC39 representatives on Matrix, as well as the post-plenary dataflow meeting in January, I decided removing function binding from this proposal while keeping function calling. Some representatives see overlap between proposals as undesirable, and function binding overlaps with partial function application. Dropping function binding will help their concerns. In addition, the function binding provided by this proposal was not that useful, since most binding involves the same object that owns the object as the receiver (e.g., rec.fn.bind(rec)), which is still clunky with this proposal (rec :> rec.fn).

After this change, it will no longer make sense for this proposal to be called “bind-this”. So we’re going to rename it to “call-this”. To distinguish between rec :> fn() (originally called “bind-this”) versus fn.@(rec) (@tabatkins’ idea, originally called “call-this”), we should now refer to them respectively as “receiver-first call-this” versus “function-first call-this”.

I will update the explainer accordingly soon.

In addition, we will be discussing this proposal’s syntax at the upcoming plenary later this month. You can view the slides now, but they are a work in progress.

hax commented 2 years ago

I decided removing function binding from this proposal while keeping function calling

A great move! Actually I started extensions proposal from my observations of two big issues of old bind op proposal:

  1. the precedence of old :: bind op is bad for method chaining ergonomics;
  2. bind-this semantics is not useful and mixing call-this/bind-this cause problems.

I'm glad to see you finally achieve the similar conclusion. Now this proposal could be the subset of extensions proposal if the syntax is rec::[fn](arg0) (my very early unpublic draft have this syntax).

hax commented 2 years ago

Syntax fn(this: rec, arg0) is possible, but might bring confusion with fn(this, arg0) and function fn(this: T, arg0: U) {}.

Syntax fn@.(rec, arg0) seems have too small benefit thanfn.call(rec, arg0) and I very doubt it could match the syntax bar.

ljharb commented 2 years ago

I feel very strongly that unless the ordering is "receiver, function, arguments", that it won't be worth it to have syntax.

hax commented 2 years ago

Now this proposal could be the subset of extensions proposal if the syntax is rec::fn (my very early unpublic draft have this syntax).

FYI, I removed rec::[fn](arg0) from extensions proposal, because I am not sure how useful it is, especially as initial design of extensions proposal, it's trivial to write:

const ::fn = fn
rec::fn(arg0)

But I'm considering remove const ::fn = fn, if that way, adding back rec::[fn](arg0) might be a good choice.

Note there is also an alternative solution in Extensions: rec::fn:call(arg0). It need a special treatment via built-in Function.prototype[Symbol.extension], and have extra benefit that rec::fn:apply and rec::fn:bind could also work, and no need extra syntax (except extensions current syntax).

bathos commented 2 years ago

I’m confused by your last comment @hax. What is the [] syntax there, or :call, etc? Is any of that part of this proposal or are these suggestions?

hax commented 2 years ago

@bathos Sorry, I'm talking about Stage-1 Extensions proposal. The original form of this proposal is initialized as alternative to the Extensions proposal.

ljharb commented 2 years ago

More accurately, both proposals are alternative forms of the original bind operator proposal: https://github.com/tc39/proposal-bind-operator

hax commented 2 years ago
rec!>fn(arg0)
rec![fn](arg0)

These two options are not ok, because ! is already a postfix operator in TypeScript (or Type as comment proposal).


rec~~fn(arg0)
rec~[fn](arg0)
rec#[fn](arg0)

These options are not ok, because they introduce new ASI hazards.

ljharb commented 2 years ago

TS has extended JS syntax at their peril, I don't think that's a legit reason to block any JS syntax.

Separately, the committee has long had consensus to never let "new ASI hazards" block new features - if someone is choosing to omit semicolons, it's on them to use tooling that can catch it for them.

bathos commented 2 years ago

Reviewing this thread I realized I’m not 100% sure which shed is being painted or that everybody’s talking about the same one, never mind what color it should be, but maybe somebody can clarify this:

Is :: gone as a sigil option? That would surprise me given the :: operator seemed to enjoy a lot of use via Babel at one time. I’ve encountered JS devs who didn’t know it wasn’t a built-in in the language hehe. That familiarity factor for at least some JS devs should probably weigh at least as much as concern for whether non-js devs(?) might mistake it for one of the disparate things :: is used to mean in another language.

(Then again, I can’t quantify that popularity and it's possible my impression of it is mistaken.)

When this proposal re-emerged it seemed like an old beloved friend returning after being lost at sea for years :) I think this proposal made good initial choices: he needed a shave and a haircut. Probably doesn’t need brain surgery or a heart transplant tho.