tc39 / proposal-promise-with-resolvers

Home Page:http://tc39.es/proposal-promise-with-resolvers/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Alternative names for `withResolvers()`

shgysk8zer0 opened this issue · comments

I am sure I'm not the only one who thinks Promise.withResolvers() does not sound right. To see the name, I would expect it to be something that you pass one or more resolves into and it returns a Promise.

I think getResolvers() makes more sense, but it's still not descriptive of what it does. It returns the promise as well.

I don't have a good name, but "extract", "expose", "unwrap", & "destructure" seem like more descriptive words that imply what it would do. What exactly would you call { promise, resolve, reject }? What would you call the object that it returns?

That's called a "deferred", colloquially, and in jQuery.

defer is a reasonable name, but es6-shim aggressively deletes Promise.defer since Chrome shipped it (despite it not being in the spec) for many years.

I've always called these "deferreds", so I'd be happy with Promise.deferred()

I've always called these "deferreds", so I'd be happy with Promise.deferred()

That makes sense, but I assumed the name withResolvers() was attempting to distance Promises from deferreds. It's the name I already use.

commented
const { promise, resolve, reject } = Promise.resolvers()

this mirrors Object.entries(), and doesn't risk any possible interpretational overload of the already existing with keyword.

Promises are a subset of Deferreds, so i'm not sure distance is required.

My reasoning for avoiding defer/deferred is that it is not a useful label for people who are not already familiar with that specific name. History aside it doesn't really make sense as a label. My understanding is that the name comes from Python Deferreds, where Deferred is Python's name for what are in EcmaScript called Promises. (And since promises are devices for deferred execution, I think that it comes across that way to people who are not familiar with Python Deferreds per se.) So a method called Promise.defer is just a synonym for Promise.promise or Promise.executeLater or something. I don't think any of those would be good labels.

Whatever we call it, users who already know this method as defer or deferred should have no problem understanding what the method does. I'd instead prefer a label that gives new developers or developers who do not frequently encounter the method a clear idea of what it does. Admittedly this is a bit hard; the idea is that Promise.withResolvers gives you a promise, along with its resolvers, so that's why I chose it to start with. But I welcome alternative proposals!

My issue with getResolvers is it sounds like you're getting just the resolvers for some reason rather than the promise together with the resolvers.

commented
  1. Promise.evert() - "to turn inside out", though the visual closeness of "evert" to "event" is undesirable
  2. Promise.create() like Object.create()
  3. Promise.parts() similar to but not .entries()
  4. I'd suggest Promise.terminals() but {resolve, reject, promise} doesn't just have ends, instead 2 categories of things atm

Why it has to be a static method? Why not change the executor to be optional? Shouldn't this much easier to use?

const promise = new Promise;
promise.resolve(1);
console.log(await promise);

// polyfill

const LegacyPromise = globalThis.Promise;
globalThis.Promise = class Promise extends LegacyPromise {
  constructor(executor) {
    super (
      executor ?? (resolve, reject) => {
         this.resolve = resolve;
         this.reject = reject;
      }
    );
  }
}

@fisker that would both mask and create bugs; it would be a very bad idea to accidentally expose a promise's resolvers.

My understanding is that the name comes from Python Deferreds, where Deferred is Python's name for what are in EcmaScript called Promises. (And since promises are devices for deferred execution, I think that it comes across that way to people who are not familiar with Python Deferreds per se.) So a method called Promise.defer is just a synonym for Promise.promise or Promise.executeLater or something. I don't think any of those would be good labels.

I'm assuming you means twisted's deferred here, in which case it's the exact same thing as we're proposing here. It's not a promise, but a promise and its resolve/reject (called callback and errback), and the .then chaining methods, all on the same object.

Whatever we call it, users who already know this method as defer or deferred should have no problem understanding what the method does. I'd instead prefer a label that gives new developers or developers who do not frequently encounter the method a clear idea of what it does.

I think this isn't a great argument to break with a naming precedent. What is a "promise"? Well developers didn't know what it was until they learned what it was. And what's a "deferred"? They'll learn it when they learn it, unless they already know it because it's a popular name for an existing pattern.

it would be a very bad idea to accidentally expose a promise's resolvers.

Promise.withResolvers() // Object {promise, resolve, reject}
new Promise // Promise {resolve, reject}

What's the difference?

@fisker new Promise() is a very easy mistake to make when intending new Promise((resolve) => resolve()) or Promise.resolve() - nobody's going to type Promise.withResolvers() by accident.

How do you think?

const promise = Promise.withResolvers();
promise.resolve(1);

That's a real "with resolvers", since resolvers are assigned to promise. 😄

@fisker i'm not sure what you're arguing. Specifically, when typing new Promise(), omitting the executor is a mistake, and it is bad language design to turn a mistake into something that quietly does something different.

How about accept an option?

const promise = new Promise({
  defer: true,
  // Or
  exposeResolvers: true,
});
// Or
const promise = Promise.create({exposeResolvers: true})
promise.resolve(1);
console.log(await promise);

That would certainly be a viable alternative.

with has been deprecated for ages. I'm not really concerned with overload there. If there is a better term, fine, but I'm not convinced we need to deliberately avoid with for that reason.

How about accept an option?

const promise = new Promise({
  defer: true,
  // Or
  exposeResolvers: true,
});
// Or
const promise = Promise.create({exposeResolvers: true})
promise.resolve(1);
console.log(await promise);

first option is a non-starter because the Promise constructor takes an executor as its only argument.

2nd option has cognitive friction (for me anyway) due to Object.create(proto, propertiesObject)

more of an aside, but naming the wrapper object variable promise is confusing due to overload with static Promise.resolve() and leading to promise.promise.then()

per #1 it seems more and more like we are necromancing Promise.defer() and should consider leaning into that

I'm assuming you means twisted's deferred here, in which case it's the exact same thing as we're proposing here. It's not a promise, but a promise and its resolve/reject (called callback and errback), and the .then chaining methods, all on the same object.

The current proposal has it that there's no .then method on the thing returned by Promise.withResolvers. It's just a POJO with the promise, resolve and reject. Here:

interface PromisePlusItsResolvers<T> { 
    promise: Promise<T>
    resolve: (value: T) => void
    reject: (reason?: string) => void
}

class Promise {
    static withResolvers<T>(): PromisePlusItsResolvers<T>;
}

I think this isn't a great argument to break with a naming precedent.

It's not much of a precedent -- there's no precedent in the spec, nor is it something widespread in other PLs. It exists in the ecosystem but even there Deferreds are not exactly the same thing as what Promise.withResolvers returns.

What is a "promise"? Well developers didn't know what it was until they learned what it was. And what's a "deferred"? They'll learn it when they learn it, unless they already know it because it's a popular name for an existing pattern.

Promises were a new primitive that needed a concise name, and "promise" is highly evocative of what they do. Agreed that developers were never going to be able to intuit what they were from just their name and you're right that that's okay in that case. But that's not the situation here. This is just a POJO that contains a promise along with its resolve and reject functions. We don't really need a concise name for that thing because 99 times out of 100 people are going to just destructure it anyway.

The spec calls these "capabilities", which suggests Promise.capability(), which... I don't hate, I guess. It's shorter than withResolvers, at least.

The current proposal has it that there's no .then method on the thing returned by Promise.withResolvers.

Yes, they are slightly different because Twisted doesn't have a promise object and we store the the .then on promises. They're still the same concepts.

It's not much of a precedent -- there's no precedent in the spec, nor is it something widespread in other PLs. It exists in the ecosystem but even there Deferreds are not exactly the same thing as what Promise.withResolvers returns.

An impl in the most popular language library in the ecosystem. And in the precursor to the Promise spec, and the most famous promise library, a non-standard implementation in Chrome, internal names in projects like TS and node, official APIs in Deno. There is a naming precedent in our ecosystem, and even if the the exact object has slightly different properties, they're all the same pattern.

first option is a non-starter because the Promise constructor takes an executor as its only argument.

The executor needs to be callable, so there shouldn't be a compatibility problem if we add an overload.

if we add an overload.

JavaScript does not support overloading.

Sure it does:

function overloaded(arg) {
  if (typeof arg === 'function') {
    // do something
  } else if (typeof arg === 'object' && arg !== null) {
    // do something else
  } else {
    throw new TypeError;
  }
}

@bakkot yes, you can simulate it by type checking like that. I knew someone was going to mention that, I should have preempted it 😆

more of an aside, but naming the wrapper object variable promise is confusing due to overload with static Promise.resolve() and leading to promise.promise.then()

I don't want the result be {promise, resolve} either, I want it return a Promise with .resolve property, so it's still promise.then().

I want it return a Promise with .resolve property

That's not going to happen. The language is not going to provide a helper which attaches the ability to resolve a Promise to the Promise itself.

If we go with overloading new Promise (which is unlikely anyway), then we have to make sure new Promise instanceof Promise. I think the same expectation exists with a method named create().

The language is not going to provide a helper which attaches the ability to resolve a Promise to the Promise itself.

Why it have to be attached, it can be a method on prototype, only if the promise is constructed in this way, it takes effect. (I know there will be "confusion" between Promise.resolve()/Promise.p.resolve(), but "so what")

can we keep the discussion here just focused on the naming, and open a separate issue for discussion of the return type/overloading the constructor? these are distinct questions.

An impl in the most popular language library in the ecosystem. And in the precursor to the Promise spec, and the most famous promise library, a non-standard implementation in Chrome, internal names in projects like TS and node, official APIs in Deno. There is a naming precedent in our ecosystem, and even if the the exact object has slightly different properties, they're all the same pattern.

ok fair enough, but i don't personally see that level of precedent as being persuasive in this case. presumably the value of keeping to precedent is you won't confuse people -- but as i said i think the opacity of the name is much more confusing to newcomers/casual users than the break in precedent would be to those who are familiar with the name.

I know there will be "confusion" between Promise.resolve()/Promise.p.resolve(), but "so what"

For one awkward thing, MDN will not be able to document it at the moment😅 openwebdocs/project#104

Some may also expect p.resolve() and Promise.resolve(p) to be equivalent?

Personally, I'd say just drop the "with" at the front and just have:

const { resolve, reject } = Promise.resolvers();

I'm not sure if that collides with any existing methods, but I don't feel like the "with" is necessary unless it's there to explicitly avoid a collision with an existing method.

I've always called these "deferreds", so I'd be happy with Promise.deferred()

I like this as well. We shouldn't be deterred by cognitive overloading from use of that term elsewhere/previously. And one could argue that it's actually beneficial.

Would predicating the returned value on a callback being passed allow for not adding a method at all?

const x = new Promise((resolve, reject) => {/* ... */})
// x = promise

// new way:
const x = new Promise()
// x = {promise, resolve, reject}

@mbrevda technically yes, but in actuality no, because that would mask an extremely common bug and so it wouldn't be deemed an acceptable design.

That suggestion wouldn't mask a bug, because the returned object isn't a promise at all, but rather a POJO. It would immediately break as soon as you tried to call any promise methods.

It's ruled out for a different reason - new Foo should always return a Foo. The language itself doesn't strictly enforce that (just that it returns a non-primitive), but everything in the entire web platform does so.

@tabatkins unless you awaited it, in which case it would silently noop.

I agree with your other reason as well, of course :-)

What about Promise.postpone() ?

Words like postpone and defer suggest that, like setTimeout, you are queuing some work to be done later. And that's not what this method does at all - it gives you a promise and the methods to resolve that promise. That's a pretty different thing.

Thanks all for explaining! Here's another non-method idea: add properties to the promise itself:

var promise = new Promise(/* ... */)

promise.then(/* */)
promise.resolve()

Is there something this would clash with?

@mbrevda Putting properties on the Promise itself means that the ability to read the Promise is conflated with the ability to write to the Promise, which is a very bad idea.

The referenced article makes an argument against the current design, too:

it [is] strange because you [are] constructing an object without using a constructor.

Although based on the above distinction (of internet vs external controll), perhaps some other method name ideas would include:

Promise.external()

// or

Promise.controlled()

// or

Promise.managed()

// or

Promise.extract()

// or

Promise.extrinsic()

@jnvm's suggestion looks very natural to me:

const { resolve, reject, promise } = Promise.create();

It's just another way to create a Promise, similar, in a sense, to things like Array.from(thing).

Some thoughts:

  • Promise.resolvers - confusing because it returns the reject and promise, and doesn't indicate creation
  • Promise.withResolvers - confusing for same reasons, and longer (Edit: Although, per @peetklecha's comment below, this is actually closer to indicating creation - it's closer to the "sentence structure" of Array.from(thing))
  • Promise.settlers - still confusing because it also returns the promise, and most devs probably won't know what it means (Edit: see @peetklecha's comment below)
  • Promise.defer - vast majority of devs will have no idea what this means

Whatever the choice is, I think it's a good idea for the verb to indicate that something is actually being "created" - in the same way that it's clear that Array.from(thing) is clearly creating an array.

I guess another possibility is to be even more explicit with something like Promise.create({withSettlers:true}) (or Promise.create({withResolvers:true})), but that is maybe a little cumbersome - depends on how commonly this will be used I guess.

  • Promise.resolvers - confusing because it returns the reject and promise, and doesn't indicate creation
  • Promise.withResolvers - confusing for same reasons, and longer
  • Promise.settlers - still confusing because it also returns the promise, and most devs probably won't know what it means

To be very clear (and to also clarify something I mis-said during this past TC39 meeting), resolvers is the correct terminology. The resolve function does not always settle the promise, it could also lock the promise in to the fate of another promise while leaving it in a pending state. The term resolve means: to determine the fate of the promise, i.e., to settle it directly or to lock it in to the fate of another promise. So fulfillment and rejection are both forms of resolution. So it is correct to say that resolve and reject are both resolvers.

See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise .

Whatever the choice is, I think it's a good idea for the verb to indicate that something is actually being "created" - in the same way that it's clear that Array.from() is clearly creating an array.

I'm glad you bring up Array.from because it is inspiration for Promise.withResolvers, namely the template <class>.<prepositionalphrase>. In the same way that Array.from(thing) gives you an array from thing, Promise.withResolvers() gives you a promise with resolvers.

indeed, they are both called "resolving functions" in the spec

Promise.destructure() from the OP hasn't gotten much attention. I don't hate it.

That implies to me the same thing as "unwrap", that it takes a promise and synchronously produces the result.

commented

Curious how viable resolve and reject are as instance methods on Promise.

I don't think I've seen anyone suggest Promise called as a function:

const { promise, resolve, reject } = Promise();

It seems to meet all of our criteria without having to choose a method name.

...Except that overloading the call and construct signatures is probably a bad design post-ES6?

[[Call]] and [[Construct]] are different all the time, usually so one or the other will unconditionally throw. In a "post-ES6" world, it's like writing

constructor() {
  if (new.target !== undefined) {
    throw new TypeError;
  }
  // ...
}

It seems to meet all of our criteria without having to choose a method name.

perhaps.. my initial thought is that it induces the ick wrt call vs construct, cognitive momentum, and potential for confusion.

is it better in some way than a named method on Promise or is it just an anti-bikeshedding solution?

I think I'm starting to like it, but also seems like it would introduce a (minor) footgun, so maybe not

What's the footgun? That somebody accidentally uses new?

const { promise, resolve, reject } = Promise()

JS is weird enough as it is around including new vs not, and I think this would just add some more confusion/inconsistency for the average dev (like me). It looks to me like that should return a Promise, rather than an object with a Promise in it. Maybe there's a precedent for this sort of thing that I'm not aware of?

Also, I also agree with an earlier comment that Promise.destructure() hasn't had enough attention - I initially thought it was overloading the term a bit too much, but, on reflection, it seems like an analogy that would be "permissible" to the mental models of most devs, and it avoids confusion around reject actually being a "resolver", technically speaking.

commented

I wouldn't mind if Promise.withResolvers moves forward as is. It's explicit, and it tells you about its relationship with Promise by being a static method on it.

If that isn't a constraint, however, I'd like to propose a new class.

const resolvable = new Resolvable()

{
    ...
    resolvable.fulfill(value)
    ...
    resolvable.reject(reason)
    ...
}

return resolvable.promise

Resolvable implies to me that other promises aren’t resolvable.

Getting back into the conversation here...

I get that withResolvers() implies "promise and resolvers" now. With it being actually said, it kinda makes sense now. But I still think that it could very easily be confused.

I think that a better return value, to better fit the name, would be:

const [promise, { resolve, reject }] = Promise.withResolvers();

Further, at least to account for the other implication of the name, I think it maybe should [optionally] accept arguments for resolve and reject, defined externally to the promise:

function resolve() {
  /*...*/
}

function reject() {
  /*...*/
}

const [promise] = Promise.withResolvers(resolve, reject);

resolve('foo');

The former makes more sense to me with what the intended meaning is, and the latter is more along the lines of what I'd initially expect of it. I think that returning [promise, { resolve, reject }] is reasonable and works well for both scenarios, and allows for resolve and reject to be passed in.

I also think that being able to pass in resolve and reject potentially make it more versatile and flexible since the conditions for resolve/reject could be imported and reused (eg a form submit or reject events, clicking certain buttons, a timeout/AbortSignal.timeout etc).

@shgysk8zer0

I also think that being able to pass in resolve and reject potentially make it more versatile and flexible...

Wouldn't that be an alternate execution / subscription model and be out of scope for naming this helper utility?

external resolver thoughts

This inverts the promise in a way that unhelpfully hides how the promise mananges its state and invokes subscribers. I was thinking it would break (like couldn't work) but maybe I'm just not creative enough to figure out how to subscribe to a callback function being invoked. It's cleaner to return the resolver so the promise can hook into its invocations while managing its state rather than accept a callback and listen to this external callback's invocation.

Alsl what if resolve is invoked immediately? Do JS functions remember they are involved and would the promise instance now need to check that? Just seems far simpler to tie all resolvers existences to the promise instance directly and provide helpers to hook in after that, not before.

.deferred / .create / .withResolvers wrap the current model with a noop helper that extracts the resolvers rather than providing an alternate model for creating or controlling a promise.

Could be off my rocker, happy to spar more, just seems like the kind of thing that 1) is not on point for this discussion and 2) would be dismissed almost off-hand either as an incompatible breaking change or as incurring additional complexity for implementors.

Thanks to everyone for participating in this discussion. Although I have never considered withResolvers to be a perfect name, I haven't been compelled by any alternatives. The history-based defer/deferred has gotten the most discussion. Many other proposals have been suggested here but none have received any traction. So in advance of requesting Stage 3 at next week's TC39 plenary, I'm closing this issue and moving forward with withResolvers.

To summarize my own rationale: withResolvers (when read together with the class, which according to spec must be the receiver, barring subclassing cases) is totally descriptive of what it does: Promise.withResolvers gives you a promise, along with its resolver functions. The only real alternative presented has been defer/deferred on the basis of those names having some historical usage in libraries. I think users who are already familiar with defer will no trouble adapting to the new name and understanding what this method does. Users who are not already familiar with defer, on the other hand, will be greatly benefitted, I think, by having a transparent, descriptive name.

Thanks again for everyone's participation!

I am not agree with a name #17.

commented

Sorry I'm late to the party. If there's still a chance, consider "andResolvers" because to me, "withX" sounds like it adds an optional feature X to the Promise.