Skip to content
ECMAScript Wavy Dot ("~.") Syntax: Support for chaining Promises
Branch: master
Clone or download
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
README.md removed stale text. made separate tokens Sep 20, 2019

README.md

ECMAScript Wavy Dot Syntax: Support for chaining Promises

By Mark S. Miller (@erights), Chip Morningstar (@FUDCo), and Michael FIG (@michaelfig)

ECMAScript Wavy Dot Syntax: Support for chaining Promises

Summary

Promises in Javascript were proposed in 2011 at the ECMAScript strawman concurrency proposal. These promises descend from the E language via the Waterken Q library and Kris Kowal's Q library. A good early presentation is Tom Van Cutsem's Communicating Event Loops: An exploration in Javascript. All of these are about promises as a first step towards distributed computing, by using promises as asynchronous references to remote objects.

Kris Kowal's Q-connection library extended Q's promises for distributed computing, essentially in the way we have in mind. However, in the absence of platform support for Weak References, this approach was not practical. Given weak references, the Midori project and Cap'n Proto, among others, demonstrates that this approach to distributed computing works well at scale.

The old ECMAScript strawman concurrency proposal also described a simple desugaring of an infix bang (!) operator to allow chaining of remote-able promise operations. To avoid conflict with TypeScript, this proposal instead introduces the wavy dot syntax (~.) syntax, with simple semantics integrated with extensions to the Promise API. We explain the notion of a handled Promise, which extends a given Promise to implement different wavy dot behaviors. These mechanisms, together with weak references, enable writing remote object communications systems, but they are not specific to any one. This proposal does not include any specific usage of the mechanisms we propose, except as a motivating example and test of adequacy.

Details

In contrast to async/await, wavy dot is designed to allow the convenient chaining of Promises without interrupting evaluation. The specific mechanism used by wavy dot allows us to implement remote network protocols taking advantage of Promise Pipelining.

Wavy Dot

Wavy dot (~.) is a proposed operator with the same precedence as dot (.).

The Synchronous Syntax column describes the analogous synchronous operation on plain objects, while the Promise Syntax introduces the proposed Promise-based operations. The Promise.prototype API additions needed for each Promise Expansion are explained in the following section.

Synchronous Syntax Promise Syntax Promise Expansion
x[i](y, z) x ~. [i](y, z) Promise.resolve(x).post(i, [y, z])
x.p(y, z) x ~. p(y, z) Promise.resolve(x).post('p', [y, z])
x(y, z) x ~. (y, z) Promise.resolve(x).post(undefined, [y, z])
x[i] x ~. [i] Promise.resolve(x).get(i)
x.p x ~. p Promise.resolve(x).get('p')
x[i] = v x ~. [i] = v Promise.resolve(x).put(i, v)
x.p = v x ~. p = v Promise.resolve(x).put('p', v)
delete x[i] delete x ~. [i] Promise.resolve(x).delete(i)
delete x.p delete x ~. p Promise.resolve(x).delete('p')

Default Behaviour

The proposed Promise.prototype API additions have the following behaviour. In the examples below, p is a promise and t is the resolution of that promise. The Default Behaviour implements the same basic effect as the Synchronous Syntax column in the previous section, but operates on a promise. The Handled Behaviour is described in the next section:

Method Default Behaviour Handled Behaviour
p.post(undefined, args) p.then(t => t(...args)) h.POST(t, undefined, args)
p.post(prop, args) p.then(t => t[prop](...args)) h.POST(t, prop, args)
p.get(prop) p.then(t => t[prop]) h.GET(t, prop)
p.put(prop, value) p.then(t => (t[prop] = value)) h.PUT(t, prop, value)
p.delete(prop) p.then(t => delete t[prop]) h.DELETE(t, prop)
p.invoke(optKey, ...args) p.post(optKey, args) (post shorthand only)
p.fapply(args) p.post(undefined, args) (post shorthand only)
p.fcall(...args) p.post(undefined, args) (post shorthand only)

Handled Promises

In a manner analogous to Proxy handlers, a promise can be associated with a handler object that provides handler methods (POST, GET, PUT, and DELETE) as described in the previous section, to override its normal unhandled behaviour. A handled Promise is constructed via:

// create a handled promise with initial handler:
Promise.makeHandled((resolve, reject) => ..., handler);
// or when resolving a handled promise:
resolve(t, handler)

This handler is not exposed to the user of the handled Promise, so it provides a secure separation between user mode (where the wavy dot syntax is used) and system mode (which implements the communication mechanism).

Below are some handled Promise examples:

//////////////////////
// Create a handled Promise, whose behaviour queues messages
// until resolve is called below.

const executorDefault = (resolve, reject) => {
  // Resolve the promise with target and use the default behaviour.
  setTimeout(() => resolve('some value'), 3000);
};

const targetP = Promise.makeHandled(executorDefault);

// These methods can run anytime before or after the targetP is fulfilled.
// The default handler for makeHandled queues the messages until the fulfill.
targetP.then(val => assert(val === 'some value'));
targetP ~. length.then(len => assert(len === 10));
targetP ~. concat(' foobar').then(val => assert(val === 'some value foobar'));

///////////////////////
// Create a handled Promise that queues messages for a remote slot.
const handler = {
  GET(o, prop) {
    return (...args) => queueMessage(slot, prop, args);
  },
  POST(o, prop, args) {
    return queueMessage(slot, prop, args);
  },
  // Unimplemented handler methods throw errors if invoked.
};

const executorFulfilled = (resolve, reject) => {
  setTimeout(() => {
    // Resolve the target Promise with target and continue to associate handler with it.
    resolve(target, handler);
  }, 1000);
};

// Create an unfulfilled target Promise, associated with the handler.
const targetP = Promise.makeHandled(executorFulfilled, handler);

targetP ~. foo(a, b, c) // results in: queueMessage(slot, 'foo', [a, b, c]);
targetP ~. foo ~. (a, b, c) // same

If the handler (second argument) is not specified to resolve, the handler is the same one that has previously been assigned to target, or else as described in the Default Behaviour section. If a handler does not implement a Handler Method, then a TypeError is thrown if the client code calls a method that relies on it.

Promise Pipelining

The Promise pipelining mechanism allows enqueuing messages and immediately returning references that correspond to their results, without a network round trip. Handled Promises are designed specifically to allow transparent implementation of this mechanism.

In the above example, the queueMessage function would decide how and when to send messages destined for a slot. Even if the slot's destination is not yet determined, the message can be enqueued for later delivery. Once the destination is resolved (in the executor), the enqueued messages can be delivered, and further messages also can be handled by queueMessage.

Proposed Syntax

Abstract Syntax:

 Expression : ...
      Expression ~. [ Expression ] Arguments    // eventual post
      Expression ~. Arguments                   // eventual post
      Expression ~. [ Expression ]              // eventual get
      Expression ~. [ Expression ] = Expression // eventual put
      delete Expression ~. [ Expression ]       // eventual delete

Attempted Concrete Syntax:

  MemberExpression : ...
      MemberExpression ~ . [ Expression ]
      MemberExpression ~ . IdentifierName
  CallExpression : ...
      CallExpression ~ . [ Expression ] Arguments
      CallExpression ~ . IdentifierName Arguments
      MemberExpression ~ . Arguments
      CallExpression ~ . Arguments
      CallExpression ~ . [ Expression ]
      CallExpression ~ . IdentifierName
  UnaryExpression : ...
      delete CallExpression ~ . [ Expression ]
      delete CallExpression ~ . IdentifierName
  LeftHandSideExpression :
      Identifier
      CallExpression [ Expression ]
      CallExpression . IdentifierName
      CallExpression ~ . [ Expression ]
      CallExpression ~ . IdentifierName

Implementation

There is a shim for the proposed Promise API additions, which makes it possible to use the expanded forms needed for wavy dot, but not the wavy dot syntax itself.

TODO: Fix the following REPL so it actually implements wavy dot instead of infix bang.

You can experiment with the wavy dot syntax desugaring in the Infix Bang REPL. The following code fragments can be used as input:

x ~. p(y, z, q)
x ~. [i](y, z)
x ~. (y, z)
x ~. ()
x ~. p
x ~. [i]
x ~. p = v
x ~. [i] = v
delete x ~. p
delete x ~. [i]

Caveats

To fully implement promise pipelining requires more support from the handled Promises API. We will require at least one new hook to notify the handler when a promise resolves to another promise, so that messages destined for the prior promise can be re-enqueued for the new promise. This change can be introduced in a later stage of this proposal while maintaining backward compatibility.

You can’t perform that action at this time.