[strawman] Symbol.thenable proposal
It's worth noting that the driving use case here is coming from NodeJS development hitting issues where the guaranteed result for dynamic import resolution can't be assumed to be a module namespace, although please correct me if I'm wrong here Gus.
Alternatively could this mitigation be handled by creating a
Promise.resolveStrict
primitive that explicitly opts-out of thenable
resolution in the promise chain?
I'd think we are getting further and further away now from third-party promise implementation interops, that such approaches might make sense to consider at this point, in the name of returning to more well-defined semantics.
await import(path)
wouldn't ever be able to do anything besides
Promise.resolve; I'm pretty confident that this proposal, or something like
it, is the only possibility to make ModuleRecords (for modules that export
a then
function) not be considered thenable.
I can't remember where, but I recall seeing this discussed elsewhere (maybe in the TC39 meeting notes?) and the conclusion was basically ¯_(ツ)_/¯. I'm not convinced myself it's actually worth the extra symbol just to make something not considered a thenable - all these Promise libraries have been able to get away with it for this long; what makes ES promises any different here? (Dynamic import is probably the only possible case I can think of short certain proxies in terms of things that could be considered thenables but shouldn't always.)
Worst case, you can just return a value that happens to have a promise
in a property, like in {value: somePromise}
- nobody resolves that
except co
IIRC.
Isiah Meadows me at isiahmeadows.com
Looking for web consulting? Or a new website? Send me an email and we can get started. www.isiahmeadows.com
To state even more clearly how this is directly affecting API decisions in
NodeJS, we're considering supporting a dynamic import hook for vm.Script
,
and one of the API suggestions is:
new vm.Script(import('x').then(x => console.log(x))
, {
async resolveDynamicImport (specifier) {
return getNamespacefor(specifier);
}
});
The issue here being that this API is guaranteed to result in an error for
module namespaces exporting a then
function.
Yes, effectively not allowing variable dictionary object returns in promise-based APIs becomes the fix, but this does seem a rather arbitrary restriction on API development.
I think the proposal here would be a much-needed addition to be able lock-down guarantees in scenarios such as this.
Currently 'then' is effectively a reserved key and I'd expect any extension that allows the language to ignore it likewise include a means of changing the key entirely.
On Apr 12, 2018, at 9:33 PM, Gus Caplan <me at gus.host> wrote:
Hello all,
In an effort to curtail the interesting behavior of Promise.resolve (especially with regard to dynamic import), I have created a proposal for a well-known symbol which will allow an object to not be treated as a "thenable."
I am privy to the current protocol proposal which might be a better fit for this, but due to dynamic import already being stage 3, I don't feel we should wait for it to come to fruition.
Comments and suggestions are of course quite welcome at the repo 1.
Thanks, -Gus
I don't understand why you would expect that, can you explain a bit more?
A hypothetical proposal addressing that would intersect with this.
It's a fair point, i assume the usage would be like if the property exists then ignore thenable behaviour and if it is falsy then none of any of this behaviour would occur, so a Module Namespace Object could have like [whateverSymbolName]: null, if thats sorta what you are proposing?
This proposal is that Symbol.thenable can be false, or true (which is a no-op). A future proposal could make it also be a string or symbol (IsPropertyKey), and then change the method name.
In other words, I don't think there's a conflict, and I think that changing the method name could and should be a followon proposal.
Not sure how this is relevant, since this is about types rather than identity, and I'd say typing values is far from a solved problem beyond the basic concept of "all values have a type"...* ;-)
* en.wikipedia.org/wiki/Type_system
Isiah Meadows me at isiahmeadows.com
Looking for web consulting? Or a new website? Send me an email and we can get started. www.isiahmeadows.com
I mean you could have an interface IThenable that you implement to a class/constructor. Then each instantiated object of such class/constructor can be "identified" as a Thenable. Yes, that involves a type system.
An HTML attachment was scrubbed... URL: esdiscuss/attachments/20180415/6811e48a/attachment
On Fri, Apr 13, 2018 at 6:00 PM, Isiah Meadows <isiahmeadows at gmail.com> wrote:
I can't remember where, but I recall seeing this discussed elsewhere (maybe in the TC39 meeting notes?) and the conclusion was basically ¯_(ツ)_/¯. I'm not convinced myself it's actually worth the extra symbol just to make something not considered a thenable - all these Promise libraries have been able to get away with it for this long; what makes ES promises any different here? (Dynamic import is probably the only possible case I can think of short certain proxies in terms of things that could be considered thenables but shouldn't always.)
Having a reserved property key is a big footgun; you can't reliably resolve a promise to an arbitrary object, because if the object happens to have a "then" key, it'll try to recursively resolve it.
Userland may "get away with it" because "then" isn't a common key, but it's still a hassle, same as how proto being a special key in JS causes problems with JSON usage - it's rare, but not unknown, and problematic because it's an obvious layering violation.
Worst case, you can just return a value that happens to have a promise in a property, like in
{value: somePromise}
- nobody resolves that exceptco
IIRC.
Note that this isn't about nesting promises (or even promise-likes) without recursive resolution, it's about nesting thenables into a promise without (attempted) recursive resolution, which is a much larger conceptual class: the presence of a "then" property says absolutely nothing about the type of an object. We've just been making a statistical judgement about the posterior probability of an object being promise-like based on the presence of a particular key, which is very dubious.
I'm still strongly of the opinion that we messed this up; the reason we went with thenables was because it's how userland did it, and they were working under the constraints of a proliferation of promise-likes and the difficulty of userland type-testing; real Promise usage promulgated much quicker than the pessimistic estimates, tho, making the relative trade-off of "free" compatibility with promise-likes vs the layering violation of reserving a property name on every object forever much less favorable. We should have instead relied on the actual Promise type, with a Symbol escape-hatch opting a userland object into being a promise-like. Doing the reverse and letting objects opt out of being treated as promise-like is probably the most we can do at this point, unfortunately. Generic data-handling will just have to manually add such a Symbol to objects they're resolving to, which sucks but is better than the alternative of always using a wrapper object.
(Note that having a "strictResolve" method won't help; it might prevent the precise promise you construct from recursing into the object, but if you then resolve another promise to that promise, it'll go ahead and try to recurse again. Maybe strictResolve() could auto-add the "I'm not a promise-like" symbol to the object it resolves to? That would be a bit more ergonomic for generic handling.)
I agree with Tab that allowing developers to opt out of thenable behavior
is the only recourse now, but I don't think Symbol.thenable
is the best
way to grant that power.
In the case of module namespaces, since module authors cannot use
Symbol.thenable
as the name of an export, there's no way to export a
then
function while also opting out of thenable behavior by exporting
Symbol.thenable
as false
.
Instead, this proposal seems to imply that Symbol.thenable
would have to
be added automatically to every module namespace object, which could be bad
news for module authors who (for whatever reason) actually wanted to export
a then
function to make the namespace thenable.
In general, whether or not we're talking about module namespace objects,
we're beginning to realize that the presence of a function-valued .then
property is not enough information to decide thenability in all cases,
though of course it must remain the default behavior because that's how
promise libraries have been written, and folks still do use promise
libraries instead of the native Promise
(e.g. Bluebird).
If an object does not have a .then
function, then clearly it should not
be treated as thenable, so I wonder if there might be a way to disambiguate
individual then
functions according to developer intent. Specifically,
what if the developer could set object.then.able = false
to prevent
object
from being treated as thenable, even though it has a
function-valued then
property? In addition to checking for the presence
of a then
method, Promise
implementations would also need to check that
then.able !== false
before treating the object as thenable. If the
.able
property is absent from the then
function (or truthy), the normal
thenability behavior would continue to apply.
This proposal addresses the module namespace problem I described above,
since you can set then.able = false
before exporting then
from a
module. Note: because of function hoisting, it might be possible to get
access to the then
function before then.able
has been set, but I don't
think that's a problem for dynamic import()
, since the module has to
finish evaluating before the import()
promise resolves.
While I think then.able
is kinda cute (which is worth… something?), I
would also be totally open to alternatives like setting
then[Symbol.thenable] = false
. The essential idea is to use properties of
the then
function itself (rather than sibling properties of the object)
to indicate whether the object should be thenable.
Ben
His errors are volitional and are the portals of discovery. -- James Joyce
I think the purpose of this proposal is to forbid module authors from making their Module Record namespace object thenable, since doing that causes confusion.
Setting a property on the function doesn't work when the "then" is inherited but the object still wants to be non-thenable; I think that the property has to go on the object - the thing that's not thenable - not on the "then" function itself, which wouldn't even have to be touched or looked at if the object is non-thenable.
On Mon, Apr 16, 2018 at 6:36 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
I'm still strongly of the opinion that we messed this up... Doing the reverse and letting objects opt out of being treated as promise-like is probably the most we can do at this point, unfortunately.
I was going to play devil's advocate here ("Is it really too late? Probably, but we should at least explore it..."), toying with the idea of a more strict kind of promise (I called it NativePromise), but I ran into a wall which may affect Symbol.thenable = false as well:
someThirdPartyPromise.then(() => import("foo")).then(/*...*/);
That converts the promise from import()
(which understands
Symbol.thenable = false) into a non-native Promise, and thus triggers the
bug Symbol.thenable is meant to resolve, since when import()
's promise
resolves, it hands a thenable to the 3rd-party promise that doesn't handle
Symbol.thenable = false. Boom.
How does this proposal address that? Because if it doesn't, and third-party libs are supposed to update to support Symbol.thenable = false, we should at least discuss making promises opt-in via Symbol.thenable = true for a special class or type of promise.
My thought (which, again, ran into the wall above) was: Add a new type of
promise (call it NativePromise) which doesn't support thenables, just
objects with Symbol.promise = true (note I didn't call it "thenable"). (I
also toyed with well-known symbols for then
, catch
, and finally
instead, but that's probably overkill). All language-generated and
host-provided promises would be NativePromises (this would be a breaking
change for async
functions, thus impact assessment would be required).
Notes:
import()
would use NativePromise and thus not be confused by a module exporting athen
function- NativePromise would be thenable
- Promise would continue to support thenables
- NativePromise would not, and thus would not be Promises/A+-compliant
- NativePromise and Promise would be fully interoperable (both would have
Symbol.promise = true and
then
) - Actively-maintained 3rd-party promise libs like Bluebird would implement Symbol.promise = true to get full interoperability with NativePromise
- NativePromise would only have one-way interoperability with any 3rd-party lib that didn't implement Symbol.promise = true
Again, that's where I ran into the wall above, which I think impacts Symbol.thenable = false as well.
-- T.J. Crowder
An HTML attachment was scrubbed... URL: esdiscuss/attachments/20180417/2c899a03/attachment
Hello all,
I just wanted to try and push this up again as this month's meeting approaches; I would love for this to get a champion and be discussed.
Hello all,
In an effort to curtail the interesting behavior of Promise.resolve (especially
with regard to dynamic import), I have created a proposal for a well-known
symbol which will allow an object to not be treated as a "thenable."
I am privy to the current protocol proposal which might be a better fit for
this, but due to dynamic import already being stage 3, I don't feel we should
wait for it to come to fruition.
Comments and suggestions are of course quite welcome at the repo 1.
Thanks,
-Gus