When the only tool you have is subclassing, all extensions look like....

# Mark S. Miller (9 years ago)

My larger theme here is that I think promise subclassing is way overrated. OO programmers tend to treat subclassing according to the "everything is a hammer" rule. Promises do need an extension point, such as the old makeFar and makeRemote proposals explained at < strawman:concurrency#fundamental_static_q_methods>

and implemented at < tvcutsem/es-lab/blob/master/src/ses/makeQ.js#L476>, in

order to enable promise pipelining.

However, since we have (mal)invested so much effort into providing subclassing as the promise extension mechanism, when rebuilding promise pipelining on ES6 promises, we will use the subclassing extension point instead, even though it is less modular. At least pipelining is a case which does require propagation, so the ES6 subclassing mechanisms should work for these. (With some caveats to be explained in later email.)

# C. Scott Ananian (9 years ago)

Promise subclassing is super useful. prfun uses it extensively: first to override the global Promise to avoid polluting the global namespace, and then secondly to implement features like Promise#bind. I've also used it experimentally to implement "functional" promises (with a Promise#chain method) on top of ES6 Promises.

I actually had a paragraph like this in my earlier response (on the other thread) and deleted it, since it seemed off-topic there. But suffice it to say I've already used the subclass extension mechanism for Promises in a number of ways which are impossible with the more-limited and ad hoc "makeRemote" mechanism. And you say that you can implement pipelining on top of subclassing. So subclassing seems more general to me...

What's the specific use case which you can't make work with a Promise subclass?

# C. Scott Ananian (9 years ago)

On Tue, Jun 9, 2015 at 12:38 PM, Mark S. Miller <erights at google.com> wrote:

On Tue, Jun 9, 2015 at 9:29 AM, C. Scott Ananian <ecmascript at cscott.net> wrote:

Mark: The prfun library in fact uses Promise#timeout(n) instead of a TimeoutPromise subclass. But this is really a language-design question. You might as well ask why we have WeakMap() as a constructor instead of using Map#weak() or weakmapify(map). The fundamental reason is "so you can name (and thus test) the type of the object".

Do you ever test that the object returned by Promise#timeout(n) is something other than a plain promise?

As I said, I use Promise#timeout myself in my implementation, not a subclass. So I haven't written tests for TimeoutPromise myself.

But the smalltalk community has a number of places where it is worth testing whether an argument is a WeakArray. Those examples would translate directly to tests for WeakPromise.

And for TimeoutPromise you could imagine something like:

var protect = function(x) {
  return (x instanceof TimeoutPromise) ? x : TimeoutPromise.resolve(x);
};
var doSomethingImportant = function(x) {
  // ensure that we're not going to hang forever waiting for x
  return protect(x).catch(....).then(....);
};

It is rather convenient that the protect function already has a clever name: TimeoutPromise.resolve.

# Mark S. Miller (9 years ago)

On Tue, Jun 9, 2015 at 9:13 AM, Mark S. Miller <erights at google.com> wrote:

My larger theme here is that I think promise subclassing is way overrated. OO programmers tend to treat subclassing according to the "everything is a hammer" rule. Promises do need an extension point, such as the old makeFar and makeRemote proposals explained at < strawman:concurrency#fundamental_static_q_methods> and implemented at < tvcutsem/es-lab/blob/master/src/ses/makeQ.js#L476>, in order to enable promise pipelining.

However, since we have (mal)invested so much effort into providing subclassing as the promise extension mechanism, when rebuilding promise pipelining on ES6 promises, we will use the subclassing extension point instead, even though it is less modular. At least pipelining is a case which does require propagation, so the ES6 subclassing mechanisms should work for these. (With some caveats to be explained in later email.)

OMG, I had this exactly backwards! In

const r = p.then(() => q);

remoteness must not an cannot propagate from p to r, since the callback is necessarily local. Remoteness propagates from p to r only for .get, .put, .post, .delete, and .there -- none of which yet exist in the std.

Worse, the std does not provide for interaction between q and r that would enable promise pipelining when q is remote. What is needed is that, when q is an unsettled remote promise, resolving r to q leaves r unsettled but gives all message pending in r, both present and future, to q so q can forward them to the remote machine.

Sigh. Even though ES6 promises stayed fairly close to E promises (where makeFar and makeRemote originated), getting from ES6 promises to remote promises with promise pipelining is going to be more work than I thought.

# Mark S. Miller (9 years ago)

On Tue, Jun 9, 2015 at 9:35 AM, C. Scott Ananian <ecmascript at cscott.net>

wrote:

Promise subclassing is super useful. prfun uses it extensively: first to override the global Promise to avoid polluting the global namespace, and then secondly to implement features like Promise#bind. I've also used it experimentally to implement "functional" promises (with a Promise#chain method) on top of ES6 Promises.

I actually had a paragraph like this in my earlier response (on the other thread) and deleted it, since it seemed off-topic there. But suffice it to say I've already used the subclass extension mechanism for Promises in a number of ways which are impossible with the more-limited and ad hoc "makeRemote" mechanism. And you say that you can implement pipelining on top of subclassing. So subclassing seems more general to me...

What's the specific use case which you can't make work with a Promise subclass?

Funny enough, I replied before I saw this. The use case I can't make work using only subclassing as an extension mechanism is promise pipelining.

# C. Scott Ananian (9 years ago)

I think that Promise pipelining works just fine -- you just have to implement it inside Promise#get, Promise#put, etc.

// This is already in prfun
Promise.get = function(fieldname) { return this.then(function(o) { return
o[fieldname]; }); };
// This comes with your RPC mechanism
RemoteObjectPromise.get = function(fieldname) {
   // note that this.handle is a name for the remote object, it is not a
resolved value.
   // as such, this method doesn't have to wait until the remote object
corresponding to
   // this.handle is resolved
   return new RemoteObjectPromise('GET', this.handle, fieldname);
};
class RemoteObjectPromise extends Promise {
  this(args...) {
    let res, rej;
    super((a,b)=>{res=a;rej=b;});
    this.handle = gensym();
    // first argument is "destination" of the operation
    rpcCall(this.handle, ...args).then( v => res(v), e => rej(v) );
  }
}

--scott ​

# Mark S. Miller (9 years ago)

What about the interaction between q and r in .then, that I mentioned in my previous email? That's the reason I changed my mind and now think we need an extension mechanism besides subclassing in order to make pipelining work. Note: it needs to work even if p is a plain promise. It is only q that knows that the scenario is special.

# C. Scott Ananian (9 years ago)

On Tue, Jun 9, 2015 at 1:43 PM, Mark S. Miller <erights at google.com> wrote:

On Tue, Jun 9, 2015 at 10:38 AM, C. Scott Ananian <ecmascript at cscott.net> wrote:

I think that Promise pipelining works just fine -- you just have to implement it inside Promise#get, Promise#put, etc.

// This is already in prfun
Promise.get = function(fieldname) { return this.then(function(o) { return
o[fieldname]; }); };
// This comes with your RPC mechanism
class RemoteObjectPromise extends Promise {
  this(args...) {
    let res, rej;

if (typeof(args[0]) !== 'string') throw new TypeError("Not a remote

reference!");

super((a,b)=>{res=a;rej=b;});
this.handle = gensym();
// first argument to rpcCall is "destination" of the operation
rpcCall(this.handle, ...args).then( v => res(v), e => rej(v) );

},

get(fieldname) {

// note that this.handle is a name for the remote object, it is not a

resolved value. // as such, this method doesn't have to wait until the remote object corresponding to // this.handle is resolved return new RemoteObjectPromise('GET', this.handle, fieldname);

}

}


What about the interaction between q and r in .then, that I mentioned in my previous email? That's the reason I changed my mind and now think we need an extension mechanism besides subclassing in order to make pipelining work. Note: it needs to work even if p is a plain promise. It is only q that knows that the scenario is special.

Oh, right, I forgot:

RemoteObjectPromise[Symbol.species] = Promise;

That takes care of then()...

And provides another nice use case for species.

What would you expect RemoteObjectPromise.all() to do in this case?

Probably RemoteObjectPromise.prototype.all() makes more sense -- if you have a remote reference to a (promised) array, you can pipeline the wait for all the elements in the array, without having to do all the waits on the client side.

But in this case RemoteObjectPromise.prototype.all() is probably special-cased (and ignores species):

RemoteObjectPromise.prototype.all = function() {
  return new RemoteObjectPromise('ALL', this.handle);
};
// And this is what I claim that `Promise.all` should also be:
RemoteObjectPromise.all = function(x) { return this.resolve(x).all(); };

Note that, given the definition above (I've tweaked it slightly), RemoteObjectPromise.resolve(x) throws TypeError if x is not already a remote object reference. You could tweak it to perform a migration or some such if you preferred.

# Mark Miller (9 years ago)

I don't understand your answer. p and r are plain promises. They must be because they exist before q does. q is the first remote promise in this scenario. How does resolving r to q leave r unsettled but give q access to the messages buffered (and to be buffered) in r?

I think the answer must involve the to-be-defined behavior of .get, .put, .post, .delete, and .where. These should be in bed with the implementation of the built-in promise resolve, so that when r is resolved to q, if q is a genuine promise (whatever that means in this new world), then r should migrate its messages there.

However, to implement this on ES6 without monkey patching would require intervening in the internal resolving of r while still leaving r unsettled. If q intervenes by overriding .then, this does give it control during the resolution. But it doesn't give it access to r with which the buffered messages is associated.

I don't get it.

# C. Scott Ananian (9 years ago)

On Tue, Jun 9, 2015 at 3:45 PM, Mark Miller <erights at gmail.com> wrote:

I don't understand your answer. p and r are plain promises. They must be because they exist before q does. q is the first remote promise in this scenario. How does resolving r to q leave r unsettled but give q access to the messages buffered (and to be buffered) in r?

const r = p.then(() => q); // q is a RemoteObjectPromise

I don't see any problem here. Perhaps you should work through the logic in Promise#then yourself to see that this works. Or play with the following in babel:

// babel doesn't implement this correctly:
/*
class RemotePromise extends Promise {
  static get [Symbol.species]() { return Promise; }
}
*/
// -- so we need to do this: --
let RemotePromise = function(f) {
  let p = new Promise(f);
  Object.setPrototypeOf(p, RemotePromise.prototype);
  return p;
};
Object.setPrototypeOf(RemotePromise, Promise);
Object.defineProperty(RemotePromise, Symbol.species, {
  get: function() { return Promise; }
});
RemotePromise.prototype = Object.create(Promise.prototype);
RemotePromise.prototype.constructor = RemotePromise;
// -- end crazy babel workaround --

let q = RemotePromise.resolve(42); // cheat, this would be the result of an
RPC call
let p = Promise.resolve({}); // local promise
const r = p.then(() => q); // q is a RemoteObjectPromise

r.then(function() {
  // show that this works properly!
  console.log(q instanceof RemotePromise, p instanceof RemotePromise, r
instanceof RemotePromise);
});

I think where you are surprised is because (in this model) p.then() is implicitly requesting a local computation. Even though () => q in your

sample code looks like a no-op, because q is a RemoteObjectPromise and p is not, you are explicitly requesting that the result of q be transferred to the local client so it can be operated on locally. If you want to continue doing remote computation, you need to use the "remote computation" methods (get/post/delete/call/etc) which keep the result on the remote server (and all of which yield a RemoteObjectPromise, not a Promise).

This may differ from the E model (I don't know), but it makes good sense to me. You can't expect to do arbitrary computation inside a then unless the result has been transferred to the local client.