There are two proposals that do similar things.
- A proposal for an Extensions system:
::{ extFn } = obj
,obj::extFn
, andobj::extNamespace:extFn
. - A proposal for a bind-
this
operator:obj->fn
andobj->(fn)
.
Extensions system | Bind-this operator
|
---|---|
The extensions system adds
a special variable namespace
to every lexical scope,
which is denoted by a const ::has = Set.prototype.has;
s::has(1); The first line assigns |
In contrast, the bind- const $has = Set.prototype.has;
s->$has(1); This is nothing new: developers already have to be careful with their variables’ names, and they have already developed their own naming systems. |
The extensions system lets us extract objects’ property descriptors into a single variable in the special variable namespace. We would use special syntax for getting and setting extracted properties. const ::size =
Object.getOwnPropertyDescriptor(
Set.prototype, 'size');
// Calls the size getter.
s::size; Here, the |
const { get: getSize } =
Object.getOwnPropertyDescriptor(
Set.prototype, 'size');
// Calls the size getter.
s->getSize(); We would use the getter/setter functions as usual with no special syntax. |
When extracting multiple properties from a prototype,
it is convenient to use destructuring syntax.
The extensions system provides a special const ::{
has,
add,
size,
} from Set;
// Automatically extracts from Set.prototype
// because Set is a constructor.
s::has(1);
s::size; |
With the bind- const {
has: $has,
add: $add,
} = Set.prototype;
const { get: $getSize } =
Object.getOwnPropertyDescriptor(
Set.prototype, 'size');
s->$has(1);
s->$getSize(); |
Furthermore, when protecting code against prototype pollution, this occasional clunkiness may become moot anyway with Jordan Harband’s getIntrinsic proposal. // Our own trusted code,
// running before the adversary.
// We must get these intrinsics separately.
const $has =
getIntrinsic('Set.prototype.has');
const $add =
getIntrinsic('Set.prototype.add');
const { get: $getSize } =
getIntrinsic('Set.prototype.size');
const s = new Set([0, 1, 2]);
// The adversary’s code.
delete Set;
delete Function;
// Our own trusted code, running later.
s::has(1);
s::size; |
With getIntrinsic, extracting property getters (and setters) becomes as ergonomic as extracting methods. After all, we have to get the methods separately anyway. // Our own trusted code,
// running before the adversary.
// We must get these intrinsics separately.
const $has =
getIntrinsic('Set.prototype.has');
const $add =
getIntrinsic('Set.prototype.add');
const { get: $getSize } =
getIntrinsic('Set.prototype.size');
const s = new Set([0, 1, 2]);
// The adversary’s code.
delete Set;
delete Function;
// Our own trusted code, running later.
s->$has(1);
s->$getSize(); |
The extensions system’s If the left-hand side is not a constructor, then it calls its right-hand side as a static method. const ::{
hasOwnProperty as owns,
} from Object;
for (const key in obj) {
if (o::owns(key)) {
console.log(key);
}
} |
This static-method-calling functionality overlaps with the pipe operator, which similarly allows postfix chaining. However, the pipe operator is more versatile: it is allowed with any kind of expression. // Our own trusted code,
// running before the adversary.
const {
hasOwnProperty: owns,
} = Object;
for (const key in obj) {
if (o |> owns(^, key)) {
console.log(key);
}
} |
The extensions system isolates extracted properties in their own special variable namespace, with its own lexical name resolution. The separate lexical namespace has two intended benefits:
const ::{
has,
add,
size,
} from Set;
const s = new Set([0, 1, 2]);
s::has(1);
s::size; |
But the extensions system’s special variable namespace is very contentious, as illustrated by its 2020-11 meeting notes. Several committee members have signaled that they will probably block any proposal that introduces such a new special variable namespace. In contrast, the bind- const $ = {
has: getIntrinsic('Set.prototype.has'),
add: getIntrinsic('Set.prototype.add'),
{ get: getSize }:
getIntrinsic('Set.prototype.size'),
};
const s = new Set([0, 1, 2]);
s->($.has)(1);
s->($.getSize)(); |
It is true that, sometimes, identifiers have ambiguous names. In this example, the homonymous “map”s refer to both a verb and a noun. If they have truly identical identifiers, then shadowing will occur. The verb “map” is distinguished from the noun “map” by being in the special variable namespace. // ::map is verb
const ::{ map } from Array;
// map is noun
let map = new Map();
function foo (arr) {
// ::map is verb
arr::map(f);
} |
But the special variable namespace does not provide much additional benefit to developers.
There are many words like this in English and other human languages,
not just for the names of extension methods. // $map is verb
const { map as $map } from Array;
// map is noun
let map = new Map();
function foo() {
// $map is verb
a->$map(f);
} |
The extensions system also adds a ternary operator that allows referring inline to properties from a specific constructor object’s prototype, rather than variables from the current lexical scope’s special variable namespace. s::Set:has(1);
// This is equivalent:
const ::has = Set.has;
s::has(1); |
This is similar to how the bind- s->(Set.has)(1);
// This is equivalent:
const $has = Set.has;
s->$has(1); |
The extensions system envisions developers frequently extracting quasi-extension methods from built-in prototypes. It is for this reason that the system’s ternary operator implicitly extracts prototype methods from constructor objects. indexed::Array:map(x => x * x);
indexed::Array:filter(x => x > 0);
const ::{
map, filter,
} from Array;
indexed::map(x => x * x);
indexed::filter(x => x > 0); |
This use case is made clearer with explicit code that explicitly accesses or extracts properties from the prototypes: serving as more syntactic salt. The bind- indexed->(Array.prototype.map)(x => x * x);
indexed->(Array.prototype.filter)(x => x > 0);
const {
map: $map, filter: $filter,
} = Array.prototype;
indexed->$map(x => x * x);
indexed->$filter(x => x > 0); |
The extensions system’s ternary operator,
like the import * as _ from 'lodash';
[0, 1, 2]::_:take(2);
// This is equivalent to
// _.take([0, 1, 2], 2). If the middle operand is not a constructor, then it calls its right-hand side as a static method on the middle operand, with the left-hand side as its first argument. In the previous two examples, |
This static-method-calling functionality also overlaps with the pipe operator. import * as _ from 'lodash';
[0, 1, 2] |> _.take(^, 2);
// This is equivalent to
// _.take([0, 1, 2], 2). The pipe operator is more versatile, being able to work with any expression. |
Because fetch('simple.wasm')
::WebAssembly:instantiateStreaming(); |
But the pipe operator also would also linearize this nested expression. fetch('simple.wasm')
|> WebAssembly.instantiateStreaming(^); |
Because of the polymorphism of the extensions system’s ternary operator
(which is based on whether its middle operand is a constructor or not),
and because obj::Object:toString();
Object.keys(obj); |
The bind- If we want to convert a static-method call into a postfix chaining form, we can use the pipe operator again. obj->(Object.prototype.toString)();
obj |> Object.keys(^); |
The extensions system can work with the symbol-based protocols proposal. The following example assumes that The example shows accessing // Qualified form.
iter::Foldable:toArray();
iter::Foldable:size;
// Unqualified form.
const ::{
toArray,
size,
} from Foldable;
iter::toArray();
iter::size; |
However, there is not that much additional benefit to the developer.
It is true that the developer has to be careful with their symbol variables’ names. This is nothing new: developers already have to be careful with their variables’ names, and they have already developed their own systems. // Qualified form.
iter[Foldable.toArray]();
iter[Foldable.size];
// Unqualified form.
const {
toArray: $toArray,
size: $size,
} = Foldable;
iter[$toArray]();
iter[$size]; |
The extensions system could serve as a replacement for the proposed extended numerics system. 1::px + 3::px;
1::CSS:px + 3::CSS:px; |
But this unfortunately does not save any characters over ordinary function calls. px(1) + px(3);
CSS.px(1) + CSS.px(3); |
The extensions system envisions library developers
writing new functions within the special variable namespace.
A special // Module `foo`
export const ::at = function (i) {
return this[
i >= 0
? i
: this.length + i
];
};
// Another module
import ::{ bindKey } from 'foo';
'Hello world'.split(' ')
::at(0).toUpperCase()
::at(-1); This may cause ecosystem schism between libraries that use the special variable namespace and libraries that use the ordinary variable namespace. |
In contrast, because the bind- // Module `foo`
export const at = function (i) {
return this[
i >= 0
? i
: this.length + i
];
};
// Another module
import $at from 'foo';
'Hello world'.split(' ')
->$at(0).toUpperCase()
->$at(-1); Libraries may export only to the single ordinary variable namespace. There is therefore little risk of ecosystem schism. |
The extensions system’s ternary operator
is further customizable with a const ::extract = {
[Symbol.extension]: {
get (target, key) {
return target[key].bind(target);
},
},
};
const user = {
name: 'hax',
greet () { return `Hi ${this.name}!`; }
};
const f = user::extract:greet;
f(); // 'Hi hax!' The value of the In this way, the extensions system’s ternary operator thus is actually a metaprogramming tool. |
The narrowly scoped bind- const user = {
name: 'hax',
greet () { return `Hi ${this.name}!`; }
};
const f = user->greet;
f(); // 'Hi hax!' |
The metaprogramming of the extensions’ ternary operator
can customize any getting, setting, and/or invocation
of its extension methods.
This metaprogramming can make the proposed eventual send more convenient.
(The following code uses seperately defined
const fileP = target
::send:openDirectory(dirName)
::send:openFile(fileName);
const contents = await fileP::send:read();
console.log('file contents', contents);
fileP::sendOnly:append('fire-and-forget'); |
However, this metaprogramming overlaps with proxy objects, which provide similar capabilities. For example, eventual send uses proxies to give its customized getting/setting/invocation behavior. When we need to wrap objects in proxies repeatedly, we can use the pipe operator. const fileP = target
|> E(^).openDirectory(dirName)
|> E(^).openFile(fileName);
const contents = await E(fileP).read();
console.log('file contents', contents);
fileP |> E.sendOnly(^).append('fire-and-forget'); |
The Extensions system is an ambitious metaprogramming proposal that attempts to solve several different problems in a unified fashion. Its logic is thus:
- “Being able to extract/bind and call methods is important but clunky. We should improve their ergonomics with syntax.”
- “Extracting get/set accessors is also important, so it should get syntax too.”
- “And the syntax for extracting get accessors should look similar to the syntax for extracting methods.” (Hence, its import-like “destructuring” syntax.)
- “And the syntax for extracting, importing, and using these should make it difficult to cause name collision with other variables.” (Hence the special variable namespace.)
- “It would also be good if the syntax could have extendable behavior.”
(Hence, metaprogramming with
Symbol.extensions
.)
However, several of these points are debatable.
-
“Being able to extract/bind and call methods is important but clunky. We should improve their ergonomics with syntax.”
Both proposals agree with this statement.
.bind
,.call
, and.apply
are very common, but they are also very clunky. -
“Extracting get/set accessors is also important, so it should get syntax too.”
This is debatable. Unlike extracting methods, extracting get accessors is uncommon.
-
“And the syntax for extracting get accessors should look similar to the syntax for extracting methods.” (Hence, its import-like “destructuring” syntax.)
This is very debatable. Get/set accessors are not the same as methods. Methods are properties that happen to be functions. Accessors are not properties; they are functions that activate when getting or setting properties.
-
“And the syntax for extracting, importing, and using these should make it difficult to cause name collision with other variables.” (Hence the special variable namespace.)
This is very controversial. Programmers already deal with name ambiguity and variable shadowing all the time with their own naming systems. A new language namespace is cognitively heavy, is complex for implementors, decreases interoperability, and may cause an ecosystem schism. Several TC39 members, such as Waldemar Horwat and Jordan Halbard, are strongly against any new syntactic namespace for identifiers (see 2020-11 meeting notes).
-
“It would also be good if the syntax could have extendable behavior.” (Hence, metaprogramming with
Symbol.extensions
.)Although the goal of solving multiple proposals’ problems with a single metaprogramming system is laudable, if the foundation it is built is not viable, then the metaprogramming system is not viable either.
In contrast, the [bind-this
operator][] is focused on one problem:
.bind
,.call
, and.apply
are very useful and very common in JavaScript codebases…- …but
.bind
,.call
, and.apply
are clunky and unergonomic.
All the other issues that the extensions system solves is either solved by the pipe operator – or already solved with existing features such as ordinary variables, namespace objects, proxies.
The extensions system is an ambitious and laudable exploration. However, it has little hope of advancing in TC39 as it is. And it tries to solve more problems than necessary.
But there is still a real need for a syntax
that makes .bind
, .call
, and .apply
less clunky and more ergonomic.
A single, simple bind-this
operator without extra features
is much more likely to reach total TC39 consensus,
would be easier for developers to reason about,
and does not carry a risk of ecosystem schism.
I think this was supposed to be
delete Set.prototype.has
:)This is not entirely true. It can be hardened against it just relies on being able to run code before anyone else (just like capturing any global/method before it can be modified).