Call-this operator
Update and bikeshedding
The call-this operator – a resurrection of the old Stage-0 bind operator and an alternative to the Stage-1 Extensions syntaxes – reached Stage 1 last October.
We are stuck on bikeshedding its syntax among four options.
Call-this operator was previously called the “bind-this” operator, but function binding has been dropped from this proposal for now. Only changing the this binding of function calls remains.
Recent news
Since the winter of 2021–22, we have also been discussing the call-this operator in the greater context of proposals for dataflow.
See the 2022-01 plenary dataflow discussion and the overflow dataflow discussion.
Due to TAB’s concerns about overlap between PFA syntax and function binding, we dropped function binding from the call-this operator (so rec~>fn() is valid, but rec~>fn is a syntax error). We can add function binding back later if we want.
We will continue this holistic dataflow discussion later in this meeting, but some comments are relevant to bikeshedding the call-this operator’s syntax.
Why a call-this operator: .call is common (1/2)
The dynamic this binding (the receiver of a function) is a fundamental part of JavaScript design and practice today. Developers need to switch the receiver of a function call for a variety of reasons, including…
Using the receiver as a context object on a method that is not in the receiver’s prototype:
// method is not in context.�import method from 'module';�method.call(context, arg0, arg1);
Wrapping a receiver’s method before calling it:
assertFunction(obj.f).call(obj, f);
// From bluebird@3.5.5.�tryCatch(item).call(boundTo, e);
Conditionally switching a call between two methods:
const method = obj.f ?? obj.g;�method.call(obj, arg0, arg1);
// From debug@4.1.1.�// createDebug is an object either for Node or for web browsers.�createDebug.formatArgs.call(self, args);
Reusing an original method on a monkey-patched object:
// From graceful-fs@4.1.15.�return fs$read.call(fs, fd, /*…*/)
Protecting a method call from prototype pollution:
// From lodash@4.17.11.�// Object.prototype.toString was cached as nativeObjectToString.�nativeObjectToString.call(value);
…and other reasons. They do all of this using .call.
Why a call-this operator: .call is common (2/2)
The dynamic this binding (the receiver of a function) is a fundamental part of JavaScript design and practice today. Developers need to switch the receiver of a function call for a variety of reasons: using the receiver as a context object on a method that is not in the receiver’s prototype, wrapping a method before calling it, conditionally switching a call between two methods, reusing an original method on a monkey-patched object, protecting method calls from prototype pollution, and more. They do all of this using .call.
Because of this, .call is one of the most commonly used functions in all JavaScript. Its frequency exceeds those even of console.log, .slice, and .push.
These statistics are derived from Gzemnid’s top-1000 NPM libraries, and .call’s results exclude transpiled code, as well as code that was obsoleted by ES class syntax. Our methodology is publicly available. A volunteer performed a thorough manual review of the data (thanks Scotty Jamison).
1,016,503 | .map |
315,922 | .call |
271,915 | console.log |
182,292 | .slice |
170,248 | .bind |
168,872 | .set |
70,116 | .push |
Why a call-this operator: .call is clunky
A call-this operator would reduce the boilerplate separating subject and verb, and it would restore the word order back to the natural subject–verb–object:
rec~>method(arg0).
From debug@4.1.1:�createDebug.formatArgs.call(self, args);�self~>createDebug.formatArgs(args);
From bluebird@3.5.5:�return isPending.call(this._target());�return this._target()~>isPending();
tryCatch(item).call(boundTo, e);�boundTo~>(tryCatch(item))(e);
From graceful-fs@4.1.15:�return fs$read.call(fs, fd, /*…*/)�return fs~>fs$read(fd, /*…*/)
In spite of its frequency, .call is clunky and poorly readable. It separates the function from its receiver and arguments with boilerplate, and it flips the “natural” word order, resulting in a verb.call–subject–object word order:
fn.call(rec, arg0).
JavaScript developers are used to reading methods in a concise subject–verb–object word order that resembles English and other SVO human languages:
rec.method(arg0).
Very common�× Very clunky�= Worth improving with syntax
|> does not improve .call’s clunkiness
Here is the clunky (and frequent) status quo again:
fn.call(rec, arg0)
Introducing the pipe operator fixes word order, but the result is even less readable. Excessive boilerplate separates the function from its receiver and arguments:
rec |> fn.call(@, arg0)
Only a separate operator can improve the word order without otherwise compromising readability:
rec~>fn(arg0)
We have been investigating whether it is possible to modify the pipe operator to address .call’s clunkiness while still addressing pipe’s other use cases (e.g., non-this-based, n-ary function calls; async function calls). We have still found none except a separate operator.
From debug@4.1.1:�createDebug.formatArgs.call(self, args);�self |> createDebug.formatArgs.call(@, args);�self~>createDebug.formatArgs(args);
From bluebird@3.5.5:�return isPending.call(this._target());�return this._target() |> isPending.call(@);�return this._target()~>isPending();
tryCatch(item).call(boundTo, e);�boundTo |> tryCatch(item).call(e);�boundTo~>(tryCatch(item))(e);
From graceful-fs@4.1.15:�return fs$read.call(fs, fd, /*…*/)�return fs |> fs$read.call(@, fd, /*…*/)�return fs~>fs$read(fd, /*…*/)
Concerns about ecosystem schism
𝘟: Some APIs (like “functional” APIs) use non-this-based ƒs.
𝘠: Some APIs (like “object-oriented” APIs) use this-based ƒs.
This schism between 𝘟 APIs and 𝘠 APIs is already is built into the language. The schism is such that prominent APIs like Firebase JS have switched from 𝘠 to 𝘟 (e.g., for module splitting).
But the call-this operator, together with |>, would make interoperability between 𝘟 and 𝘠 more fluid – and it would make the choice between 𝘟 and 𝘠 less viral – bridging the schism:
import { x0, x1 } from '𝘟';�import { y0, y1 } from '𝘠';
input |> x0(@)~>y0() |> x1(@)~>y1();
(Let’s defer further discussion about schism concerns to the holistic dataflow discussion redux, later during this plenary meeting. During this current discussion, let’s focus on bikeshedding a syntax – while assuming that a call-this operator will advance.)
“The answer to whether multiple ways or syntaxes of doing something are harmful critically depends on the duplication’s effect on APIs and how viral it is.
“Suppose we’re considering having two syntaxes 𝘟 and 𝘠 to use APIs. If module or person 𝘈 uses syntax 𝘟 which interoperates better with syntax 𝘟 than syntax 𝘠 and that pressures module or person 𝘉 to use syntax 𝘟 in their new APIs to interoperate with person 𝘈’s APIs, that virality encourages ecosystem forking and API wars. Introducing multiple such ways into the language is bad.
“On the other hand, if person 𝘈’s choice of syntax [i.e., 𝘟] has no effect on person 𝘉[’s choice of syntax, 𝘠,] and they can interoperate without any hassles, then that’s generally benign.”
From the 2022-01-27 dataflow meeting.
Four candidate syntaxes (issue #10)
1a. Receiver first (|>-like loose unbracketed)�rec :> fn(arg0) rec ~> fn(arg0)�rec !> fn(arg0) rec -> fn(arg0) �rec #> fn(arg0) rec ~~ fn(arg0)
The right-hand side would be an identifier fn, chain of identifiers obj.fn, or parenthesized expression (expr) – similarly to decorators.
This would also have loose precedence (like pipe |>).�x + 1 :> x.y().z() groups as�((x + 1) :> (x.y)()).z().
1b. Receiver first (.-like tight unbracketed)�rec:>fn(arg0) rec~>fn(arg0)�rec->fn(arg0) rec-.fn(arg0)�rec::fn(arg0) rec:.fn(arg0)
The same as syntax 1a, except with tight precedence (like dot .):�x + 1~>x.y().z() groups as�(x + ((1::(x.y)()).z()).
2. Receiver first (bracketed)�rec>[fn](arg0) rec~[fn](arg0)�rec rec#[fn](arg0)
3. Function first�rec |> fn@(@, arg0)
4. this as argument�rec |> fn(this: @, arg0)
Possible criteria for choosing the syntax
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(o0, o1))�than the status quo�(e.g., verb.call(subject, o0, o1))?
Confusability with other JS features�Is there a risk of beginners and other developers confusing the syntax with regular dot property access (e.g., subject.verb(o0, o1))?
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 2022-01-27 dataflow meeting says, “In general, some overlap is okay, but too much is bad; we have to decide this on a case-by-case basis.”
Recent opinions
JHD: Supports both pipe operator and call-this operator. Will block pipe operator if no call-this operator advances. Prefers unbracketed receiver-first style to all other styles.
JRL: Supports both pipe operator and call-this operator. Prefers unbracketed receiver-first styles to all other styles.
MM: Against advancing any syntactic dataflow proposal other than pipe operator. Uncertain opinion about call-this syntaxes.
RGN: Favors tight binding and strong visual distinction from pipe operator.
WH: Concerned about confusion between receiver-first call-this operator and ordinary . property access; concerned about ecosystem schism.
RKG: Also concerned about confusion between receiver-first call-this operator and ordinary dot property access. Would prefer unbracketed receiver-first style to have .-like tight precedence (like x :> fn).
RBN: Concerned about unclear grouping with unbracketed receiver-first style.
(Let’s defer further discussion about schism concerns to the holistic dataflow discussion redux, later during this plenary meeting. During this current discussion, let’s focus on bikeshedding a syntax – while assuming that a call-this operator will advance.)