Skip to content

Instantly share code, notes, and snippets.

@js-choi
Forked from hax/README.md
Last active October 6, 2021 04:57
Show Gist options
  • Save js-choi/eca7c67f88a2e82c3353fb11e8da46f7 to your computer and use it in GitHub Desktop.
Save js-choi/eca7c67f88a2e82c3353fb11e8da46f7 to your computer and use it in GitHub Desktop.
The extensions system and the bind operator

The extensions system and the bind-this operator

There are two proposals that do similar things.

Extensions system Bind-this operator

The extensions system adds a special variable namespace to every lexical scope, which is denoted by a :: sigil.

const ::has = Set.prototype.has;

s::has(1);

The first line assigns Array.prototype.slice to a variable ::slice, in the special variable namespace.

In contrast, the bind-this operator involves no special variable namespace. The developer needs to make sure that the variable of the extracted method ($has) does not shadow any other variables.

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 ::size variable contains a property descriptor and is used as if it were a property.

Object.getOwnPropertyDescriptor already lets us extract critical objects’ property getters and setters into ordinary variables.

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 import-like “destructuring” syntax that allows both methods and properties with getters/setters to be treated uniformly.

const ::{
  has,
  add,
  size,
} from Set;
// Automatically extracts from Set.prototype
// because Set is a constructor.

s::has(1);
s::size;

With the bind-this operator, ordinary destructuring works just as usual for methods. In contrast, getters/setters have to be extracted separately. 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.

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 import-like “destructuring” syntax also is polymorphic. It changes depending on whether its left-hand side is a constructor or a non-constructor (i.e., “namespace”) object.

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:

  • To prevent confusion between extracted properties’ names and the names of other identifiers.
  • To prevent accidental extraction of properties from a constructor object, instead of the constructor’s prototype.
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-this operator would involve no special variable namespace. Although this requires more repetition, it also allows the developer to be explicit (more syntactic salt) and to define a namespace object in the ordinary way.

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.
JavaScript developers already work around linguistic ambiguity all the time by being careful with their names – or by judiciously using namespace objects.

// $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-this operator allows its right-hand operand to be an arbitrary expression, as long as it evaluates into a function. Because it may be an arbitrary expression, it is both more verbose, more explicit, and more flexible.

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-this operator would encourage such clarity and explicitness.

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-like “destructuring” syntax const ::{ … } from …;, is also polymorphic. The ternary operator changes depending on whether its middle operand is a constructor or a non-constructor (i.e., “namespace”) object.

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, Set and Array were constructors, but in this example, _ is a non-constructor namespace object.

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 WebAssembly is not a constructor, the Eetensions ternary operator calls a static method.

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 Object is a constructor, the first statement means Object.prototype.toString.call(value). In contrast, we cannot use the ternary operator for the second statement’s Object.keys static method – because Object is a constructor.

obj::Object:toString();
Object.keys(obj);

The bind-this operator would not attempt to improve both applying Object’s prototype methods and applying Object’s static methods. Instead, it only solves applying Object’s prototype methods (while requiring the developer to be explicit about their extraction from Object.prototype – more syntactic salt).

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 iter.constructor implements some Foldable protocol, whose properties are keyed with the symbols Foldable.toArray and Foldable.size.

The example shows accessing iter’s Foldable properties using a “qualified form” (ternary operator) and an “unqualified form” (binary operator).

// 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. Foldable.toArray and Foldable.size already work with the ordinary property-access [] notation: a qualified form without the Extensions ternary operator. Likewise, assigning the symbol keys to ordinary variables, then using them with ordinary property access, gives an unqualified form, without needing the extensions binary operator.

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 import form would be required for these functions.

// 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-this operator involves no special variable namespace.

// 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 Symbol.extension property.

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 Symbol.extension property may be a “description” object with get, set, and/or invoke methods.

In this way, the extensions system’s ternary operator thus is actually a metaprogramming tool.

The narrowly scoped bind-this operator does not attempt to provide this same metaprogramming functionality.

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 send and sendOnly extension namespace objects.)

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:

  1. “Being able to extract/bind and call methods is important but clunky. We should improve their ergonomics with syntax.”
  2. “Extracting get/set accessors is also important, so it should get syntax too.”
  3. “And the syntax for extracting get accessors should look similar to the syntax for extracting methods.” (Hence, its import-like “destructuring” syntax.)
  4. “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.)
  5. “It would also be good if the syntax could have extendable behavior.” (Hence, metaprogramming with Symbol.extensions.)

However, several of these points are debatable.

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

  2. “Extracting get/set accessors is also important, so it should get syntax too.”

    This is debatable. Unlike extracting methods, extracting get accessors is uncommon.

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

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

  5. “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:

  1. .bind, .call, and .apply are very useful and very common in JavaScript codebases…
  2. …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.

@acutmore
Copy link

acutmore commented Oct 2, 2021

// The adversary’s code.
delete Set.prototype.slice;

I think this was supposed to be delete Set.prototype.has :)

There is currently no way to harden trusted code against an adversary’s mutation of Function.prototype.call or Function.prototype.bind.

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

// at start before other code runs:
const uncurryThis = Function.bind.bind(Function.call);
  // f => Function.call.bind(f)
  // f => (…args) => f.call(…args)

// or
const {apply} = Reflect;
const uncurryThis = f => (that, …args) => apply(f, that, args);

// then
const setHas = uncurryThis(Set.prototype.has);

// later
setHas(new Set([1]), 1); // true

@js-choi
Copy link
Author

js-choi commented Oct 2, 2021

@acutmore: Thanks for the comment! I’ve opened a new issue and replied there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment