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

Node examples #7

Merged
merged 15 commits into from Sep 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
219 changes: 162 additions & 57 deletions README.md
@@ -1,4 +1,4 @@
# Bind operator for JavaScript
# Bind-`this` operator for JavaScript
ECMAScript Stage-0 Proposal. J. S. Choi, 2021.

* **[Formal specification][]**
Expand All @@ -10,8 +10,8 @@ ECMAScript Stage-0 Proposal. J. S. Choi, 2021.
(A [formal specification][] is available.)

**Method binding** `->` is a **left-associative infix operator**.
Its right-hand side is an **identifier**
or an **expression** in `(` `)`,
Its right-hand side is an **identifier** (like `f`)
or a parenthesized **expression** (like `(hof())`),
js-choi marked this conversation as resolved.
Show resolved Hide resolved
either of which must evaluate to a **function**.
Its left-hand side is some expression that evaluates to an **object**.
The `->` operator **binds** its left-hand side
Expand All @@ -30,36 +30,48 @@ equivalent to `createMethod().bind(obj)`.
If the operator’s right-hand side does not evaluate to a function during runtime,
then the program throws a `TypeError`.

Function binding has equal **[precedence][]** with
**member expressions**, call expressions, `new` expressions with arguments,
and optional chains.

[precedence]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence

| Left-hand side | Example |
| ------------------------------- | ------------- |
| Primary expressions | `a->fn` |
| Member expressions | `a.b->fn` |
| Call expressions | `a()->fn` |
|`new` expressions with arguments | `new C()->fn` |
| Optional chains | `a?.b->fn` |

The bound functions created by the bind operator
The bound functions created by the bind-`this` operator
are **indistinguishable** from the bound functions
that are already created by [`Function.prototype.bind`][bind].
Both are **exotic objects** that do not have a `prototype` property,
and which may be called like any typical function.

Similarly to the `?.` optional-chaining token,
the `->` token may be **padded by whitespace**.\
From this definition, `o->f(...args)`
is **indistinguishable** from `f.call(o, ...args)`,
except that its behavior does **not change**
if code elsewhere **reassigns** the global method `Function.prototype.call`.

The `this`-bind operator has equal **[precedence][]** with
**member expressions**, call expressions, `new` expressions with arguments,
and optional chains.
Like those operators, the `this`-bind operator also may be short-circuited
by optional chains in its left-hand side.

[precedence]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence

| Left-hand side | Example | Grouping
| ---------------------------------- | ------------ | --------------
| Member expressions |`a.b->fn.c` |`((a.b)->fn).c`
| Call expressions |`a()->fn()` |`((a())->fn)()`
| Optional chains |`a?.b->fn` |`(a?.b)->fn`
|`new` expressions with arguments |`new C(a)->fn`|`(new C(a))->fn`
|`new` expressions without arguments*|`new a->fn` |`new (a->fn)`

\* Like `.` and `?.`, the `this`-bind operator also have tighter precedence
than `new` expressions without arguments.
Of course, `new a->fn` is not a very useful expression,
just like how `new (fn.bind(a))` is not a very useful expression.

Similarly to the `.` and `?.` operators,
the `->` operator may be **padded by whitespace**.\
For example, `a -> m`\
is equivalent to `a->fn`,\
and `a -> (createFn())`\
is equivalent to `a->(createFn())`.

There are **no other special rules**.

## Why a bind operator
## Why a bind-`this` operator
[`Function.prototype.bind`][call] and [`Function.prototype.call`][bind]
are very common in **object-oriented JavaScript** code.
They are useful methods that allows us to apply functions to any object,
Expand Down Expand Up @@ -107,7 +119,7 @@ delete Array.prototype.slice;
slice.call([0, 1, 2], 1, 2);
```

But this is still vulnerable to mutation of `Function.prototype`:
But this approach is still vulnerable to mutation of `Function.prototype`:

```js
// Our own trusted code, running before any adversary.
Expand Down Expand Up @@ -155,38 +167,124 @@ obj->extensionMethod();
// Compare with extensionMethod.call(obj).
```

The bind operator can also **extract** a **method** from a **class**
into a function whose first parameter becomes its `this` binding:\
for example, `const { slice } = Array.prototype; arr->slice(1, 3);`.\
It can also similarly extract a method from an **instance**
into a function that always uses that instance as its `this` binding:\
for example, `const arr = arr->(arr.slice); slice(1, 3);`.

## Real-world examples
Only minor formatting changes have been made to the status-quo examples.

<table>
<thead>
<tr>
<th>Status quo
<th>With binding
### Node.js
js-choi marked this conversation as resolved.
Show resolved Hide resolved
Node.js’s runtime depends on many built-in JavaScript global intrinsic objects
that are vulnerable to mutation or prototype pollution by third-party libraries.
When initializing a JavaScript runtime, Node.js therefore caches
wrapped versions of every global intrinsic object (and its methods)
in a [large `primordials` object][primordials.js].

<tbody>
<tr>
<td>
Many of the global intrinsic methods inside of the `primordials` object
rely on the `this` binding.
`primordials` therefore contains numerous entries that look like this:
```js
ArrayPrototypeConcat: uncurryThis(Array.prototype.concat),
ArrayPrototypeCopyWithin: uncurryThis(Array.prototype.copyWithin),
ArrayPrototypeFill: uncurryThis(Array.prototype.fill),
ArrayPrototypeFind: uncurryThis(Array.prototype.find),
ArrayPrototypeFindIndex: uncurryThis(Array.prototype.findIndex),
ArrayPrototypeLastIndexOf: uncurryThis(Array.prototype.lastIndexOf),
ArrayPrototypePop: uncurryThis(Array.prototype.pop),
ArrayPrototypePush: uncurryThis(Array.prototype.push),
ArrayPrototypePushApply: applyBind(Array.prototype.push),
ArrayPrototypeReverse: uncurryThis(Array.prototype.reverse),
```
…and so on, where `uncurryThis` is `Function.prototype.call.bind`
(also called [“call-binding”][call-bind]),
and `applyBind` is the similar `Function.prototype.apply.bind`.

[call-bind]: https://npmjs.com/call-bind

In other words, Node.js must **wrap** every `this`-sensitive global intrinsic method
in a `this`-uncurried **wrapper function**,
whose first argument is the method’s `this` value,
using the `uncurryThis` helper function.

The result is that code that uses these global intrinsic methods,
like this code adapted from [node/lib/internal/v8_prof_processor.js][]:
```js
???
// `specifier` is a string.
const file = specifier.slice(2, -4);

// Later…
if (process.platform === 'darwin') {
tickArguments.push('--mac');
} else if (process.platform === 'win32') {
tickArguments.push('--windows');
}
tickArguments.push(...process.argv.slice(1));
```
From ???.
…must instead look like this:
```js
// Note: This module assumes that it runs before any third-party code.
const {
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeSlice,
StringPrototypeSlice,
} = primordials;

// Later…
const file = StringPrototypeSlice(specifier, 2, -4);

// Later…
if (process.platform === 'darwin') {
ArrayPrototypePush(tickArguments, '--mac');
} else if (process.platform === 'win32') {
ArrayPrototypePush(tickArguments, '--windows');
}
ArrayPrototypePushApply(tickArguments, ArrayPrototypeSlice(process.argv, 1));
```

This code is now protected against prototype pollution by accident and by adversaries
(e.g., `delete Array.prototype.push` or `delete Array.prototype[Symbol.iterator]`).
However, this protection comes at two costs:

1. These [uncurried wrapper functions sometimes dramatically reduce performance][#38248].
This would not be a problem if Node.js could cache
and use the intrinsic methods directly.
But the only current way to use intrinsic methods
would be with `Function.prototype.call`, which is also vulnerable to mutation.

<td>
2. The Node.js community has had [much concern about barriers to contribution][#30697]
by ordinary JavaScript developers, due to the unidiomatic code encouraged by these
uncurried wrapper functions.

Both of these problems are much improved by the bind-`this` operator.
Instead of wrapping every global method with `uncurryThis`,
Node.js could cached and used **directly**
without worrying about `Function.prototype.call` mutation:

```js
???
// Note: This module assumes that it runs before any third-party code.
const $apply = Function.prototype.apply;
const $push = Array.prototype.push;
const $arraySlice = Array.prototype.slice;
const $stringSlice = String.prototype.slice;

// Later…
const file = specifier->$stringSlice(2, -4);

// Later…
if (process.platform === 'darwin') {
tickArguments->$push('--mac');
} else if (process.platform === 'win32') {
tickArguments->$push('--windows');
}
$push->$apply(tickArguments, process.argv->$arraySlice(1));
```

</table>
Performance has improved, and readability has improved.
There are no more uncurried wrapper functions;
instead, the code uses the intrinsic methods in a notation
similar to normal method calling with `.`.

[node/lib/internal/v8_prof_processor.js]: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/v8_prof_processor.js
[#38248]: https://github.com/nodejs/node/pull/38248
[#30697]: https://github.com/nodejs/node/issues/30697

## Non-goals
A goal of this proposal is **simplicity**.
Expand All @@ -201,23 +299,27 @@ but method extraction is **already possible** with this proposal.\
is not much wordier than\
`const slice = arr&.slice; slice(1, 3);`

**Extension getters and setters**
(i.e., extending objects with new property getters or setters
**without mutating** the object)
may **also** be useful,
and this proposal would be **forward compatible** with such a feature
using the **same operator** `->` for **property-object binding**,
in addition to this proposal’s **method binding**.
Getter/setter binding could be added in a separate proposal
using `{ get () {}, set () {} }` objects.
For example, we could add an extension getter `allDivs`
to a `document` object like so:
**Extracting property accessors** (i.e., getters and setters)
is not a goal of this proposal.
Get/set accessors are **not like** methods. Methods are **values**.
Accessors themselves are **not values**;
they are functions that activate when getting or setting properties.
Getters/setters have to be extracted using `Object.getOwnPropertyDescriptor`;
they are not handled in a special way.
This verbosity may be considered to be desirable [syntactic salt][]:
it makes the developer’s intention (to extract getters/setters – and not methods)
more explicit.

```js
const allDivs = {
get () { return this.querySelectorAll('div'); }
};
const { get: $getSize } =
Object.getOwnPropertyDescriptor(
Set.prototype, 'size');

document->allDivs;
// The adversary’s code.
delete Set; delete Function;

// Our own trusted code, running later.
new Set([0, 1, 2])->$getSize();
js-choi marked this conversation as resolved.
Show resolved Hide resolved
```

**Function/expression application**,
Expand All @@ -228,3 +330,6 @@ Instead, it is addressed by the **pipe operator**,
with which this proposal’s syntax **works well**.\
For example, we could untangle `h(await g(o->f(0, v)), 1)`\
into `v |> o->f(0, %) |> await g(%) |> h(%, 1)`.

[syntactic salt]: https://en.wikipedia.org/wiki/Syntactic_sugar#Syntactic_salt
[primordials.js]: https://github.com/nodejs/node/blob/master/lib/internal/per_context/primordials.js