Abstract References for ECMAScript

Overview and Motivation

Despite its functional programming facilities, ECMAScript exhibits a strong preference for object-oriented "left-to-right" composition using instance methods. Unfortunately, composition by means of instance methods requires that extensibility be achieved by adding function-valued properties to the object or a prototype ancestor of the object. This leads to the following difficulties:

This proposal adds support for "left-to-right" syntactic composition using a new binary operator, which does not require adding properties and methods to the object itself. It accomplishes this by introducing the concept of "abstract references". Abstract references are an extension of the Reference specification type whose referenced name component may be an arbitrary object.

This proposal also provides a solution to the "private fields" problem.

This work was inspired by relationships and is intended to supersede it.

A prototype implementation of this feature is available in esdown and in the esdown online REPL.

Specification

We introduce three new built-in symbols:

We introduce a new abstract operation:

The abstract operation GetValue becomes:

The abstract operation PutValue becomes:

The runtime semantics of the delete operator is modified as follows:

UnaryExpression : delete UnaryExpression

The only way to create a reference whose referenced name is neither a String nor a Symbol is by using the "abstract reference" operator:

MemberExpression[Yield] :
    MemberExpression[?Yield] :: IdentifierReference[?Yield]

Extensions to Built-In Types

The built-in types are extended as follows:

Function.prototype[Symbol.referenceGet] = function(base) { return this };
 
Map.prototype[Symbol.referenceGet] = Map.prototype.get;
Map.prototype[Symbol.referenceSet] = Map.prototype.set;
Map.prototype[Symbol.referenceDelete] = Map.prototype.delete;
 
WeakMap.prototype[Symbol.referenceGet] = WeakMap.prototype.get;
WeakMap.prototype[Symbol.referenceSet] = WeakMap.prototype.set;
WeakMap.prototype[Symbol.referenceDelete] = WeakMap.prototype.delete;

In addition, a new constructor named PrivateMap is added to the global object. The behavior of PrivateMap objects are identical to WeakMap objects, with the exception that PrivateMap.prototype does not contain a clear method.

The primary motivation for introducing the PrivateMap constructor is to allow the user to provide alternate garbage collection semantics. It is expected that the reachability of a value within a PrivateMap key/value pair is independent of the lifetime of the PrivateMap object.

Examples

Using an iterator library implemented as a module:

import { map, takeWhile, forEach } from "iterlib";
 
getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));

Using PrivateMap objects to implement private object state:

const _x = new PrivateMap,
      _y = new PrivateMap;
 
class Point {
 
    constructor(x, y) {
 
        this::_x = x;
        this::_y = y;
    }
 
    toString() {
 
        return `[${ this::_x },${ this::_y }]`;
    }
}

Private Declaration Syntax

The above implementation of private object state suggests the following syntactic sugar:

 
private _x, _y; // => const _x = new PrivateMap, _y = new PrivateMap; 
 
class Point {
 
    // ... 
}