proposal-call-this icon indicating copy to clipboard operation
proposal-call-this copied to clipboard

Bikeshedding syntax

Open js-choi opened this issue 4 years ago • 98 comments

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 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.

js-choi avatar Oct 02 '21 16:10 js-choi

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.

bmeck avatar Oct 06 '21 00:10 bmeck

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

ljharb avatar Oct 06 '21 01:10 ljharb

@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 avatar Oct 06 '21 01:10 js-choi

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

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

js-choi avatar Oct 06 '21 14:10 js-choi

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.

ljharb avatar Oct 06 '21 14:10 ljharb

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.

js-choi avatar Oct 06 '21 15:10 js-choi

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.

ljharb avatar Oct 06 '21 15:10 ljharb

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

jridgewell avatar Oct 06 '21 17:10 jridgewell

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.

rkirsling avatar Oct 06 '21 18:10 rkirsling

@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).

ljharb avatar Oct 06 '21 18:10 ljharb

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 ->.

rkirsling avatar Oct 06 '21 18:10 rkirsling

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

ljharb avatar Oct 06 '21 18:10 ljharb

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`

jridgewell avatar Oct 06 '21 18:10 jridgewell

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?

ljharb avatar Oct 06 '21 18:10 ljharb

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.

rkirsling avatar Oct 06 '21 18:10 rkirsling

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.

ljharb avatar Oct 06 '21 20:10 ljharb

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

rkirsling avatar Oct 06 '21 21:10 rkirsling

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.

rbuckton avatar Oct 07 '21 01:10 rbuckton

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

acutmore avatar Oct 17 '21 05:10 acutmore

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.

  • fn(rec: arg0) or fn(this: rec, arg0): this-argument style, first proposed by @rbuckton.
  • fn@(rec, arg0): call-this style; see the original explainer by @tabatkins.
  • rec::fn(arg0): bind-this style. :: might be some other operator, like !., ->, or ![ ].

js-choi avatar Jan 27 '22 19:01 js-choi

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.

bergus avatar Jan 27 '22 20:01 bergus

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.

ljharb avatar Jan 27 '22 20:01 ljharb

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.

rkirsling avatar Jan 27 '22 21:01 rkirsling

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)

theScottyJam avatar Jan 27 '22 21:01 theScottyJam

@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?

ljharb avatar Jan 27 '22 22:01 ljharb

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.

jridgewell avatar Jan 27 '22 22:01 jridgewell

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.)

rkirsling avatar Jan 28 '22 00:01 rkirsling

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.

bathos avatar Jan 28 '22 00:01 bathos

. 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.

jridgewell avatar Jan 28 '22 01:01 jridgewell

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

rbuckton avatar Jan 28 '22 01:01 rbuckton