Function.prototype.new

Times are different these days ... 'cause the whole WASM premise was to enable any PL to land in the JS kingdom, and that's what I do daily ... but also that's what happens daily regardless of what I do:

If there is one crucial and missing interoperability shenanigans between JS and many other PLs is that JS new syntax/keyword is rarely welcomed elsewhere or it means something completely different ... so that I don't know how many times this topic has been raised already, but I think it's really time to consider the smallest improvement that would improve PLs interoperability with JS by far:

Function.prototype.new = function () {
  return Reflect.construct(this, arguments);
};

That's it, that's the proposal, because when I see stuff like this (see latest example) I feel sick inside:

import pythonmonkey as pm

my_date = pm.new(pm.Date)(0)
print(my_date)

There's clearly a need to disambiguate JS classes from regular invoke-able functions in JS, not really for the JS world where these can throw happily ever after, but for all places where the intent is inevitably ambiguous.

I had this conversations with various developers from foreign PLs and agreed with all of them: new is a curse for interoperability and clarity, and the proposed function looks like the least JS can do to help disambiguating intents elsewhere.

Thanks for considering this proposal, the only alternative I could think about is about the ability to map that, hence auto-bind it when accessed, as in:

Object.defineProperty(Function.prototype, 'new', {
  configurable: true,
  get() {
    return (...args) => new this(...args);
  }
});

// mapping ...
['me', 'you', 'everybody', 'everybody'].map(User.new);

I don't think there's anything else really to consider or discuss, as I can't think of any other usage of the new keyword out of a Function.protoype ... moreover:

  • would it throw for arrows? sure, as new () => {} does already
  • would it throw for short-hand methods? see previous point
  • would it make instantiation confusing? I don't think so ... it's been adopted already by foreign PLs when writing code in their language wants to understand if an instance is being created or not (also proxy traps relevant)

That's it, happy to clarify, if needed, anything else.

edit see wasmoon shenanigans to guarantee a JS class can be instantiated, using create instead of new because they (I assume) got inspiration from the only Object.create case we have in JS these days ... all those create tests could disappear if we had new backed in, but that's not the only WASM runtime I've seen disambiguating ...

1 Like

Doesn't Reflect.construct(Constructor, args) already do that ?

I'm not sure Reflect.construct helps here. I think the motivation is to be able to, instead of this

my_date = pm.new(pm.Date)(0)

use

my_date = pm.Date.new(0)

and I believe in going with the construct route we'd be looking at something like

my_date = pm.Reflect.construct(pm.Date, 0)

which is what pm.new appears to be doing albiet more concisely.

2 Likes

To make sure I got this right, is this about "how it looks"? I don't think it justifies the cost of adding a new property to the prototype of every function.

with the current pm.new you can easily do something like

NewDate = pm.new(pm.Date)
my_date = NewDate(0)

and reuse this "constructor", which looks fairly ergonomic to me.

1 Like

it's verbose but yeah, my new this(...args) can in fact be replaced with Reflect.construct(this, args) and yes, this is all about being able, from Python, Lua, Ruby, R, or any other PL with JS bindings, to write natural code with explicit intent, that is Date.new(0) instead of any alternative you can find in the wild.

It's the good old question if any simplification should ever land when there are verbose alternatives around ... yes is my answer from a DX perspective, here it would welcome more interoperability across PLs in days these can seamlessly work togheter.

The alternative is always to pollute globals in user-land and this is an easy to do so, as there is no other meaning for having new on Function.prototype, if not to help creating explicitly classes instances, but it'd be unfortunate if two user-land use the accessor version VS the direct version so I was hoping for a yes, that's possible resolution.

@bergus new is not an operator in Python. Python classes are instantiated like function calls.

2 Likes

beside what @Josh-Cena said, where new Anything() in Python is a syntax error, JS doesn't have to change anything for this, it eventually enables nicer interoperability across multiple PLs.

It sadden me here it's very often the same loop:

  • no ... just to start with, no matter what is the request (if not from a TC39 member, of course)
  • clarficiations and reasons to explain why the request was made
  • late people joining to strenghten that no
  • eventually somebody shows up without even understanding the issue, still pivoting for no

This one in particular is extremely doable, extremely simple to explain, reason about, implement, and it would improve by far the JS <-> WASM-PL interoperability story ... what is the actual problem in adding a feature like this to welcome more foreign languages that would like to use JS without making life awkward for every users of such foreign PLs, hence welcoming new JS adopters ... eventually?

Ooops I was thinking of __new__. But of course one wouldn't spell that out…

Yes, most feature requests should be rejected; it's normal for the default to be "no" with things like this. In a brand new language you can be free-er with figuring out the ergonomic story, but as a language matures and increases usage you should naturally be more conservative.

But to the issue at hand, you keep saying it "increases interoperability" - I don't understand what you mean by this. I think you're just arguing that it would make the mental models of the languages slightly closer together? Is there actually an interop problem right now that is caused by the new keyword?

You've pointed to examples like PythonMonkey, but I don't understand the issue. PythonMonkey is trying to translate between the two languages; if it wanted to recognize the call/construct distinction that JS has but in a more natural manner, it could pretend that all JS classes have a .new() method, all by itself. We don't need to bless this prototype method for it. (And it would be exactly as compatible - whether the JS engine or the PythonMonkey interpreter adds the method, it can still be accidentally overridden by userland code.)

I think the only actual argument here is ergonomics - prefix operators kinda suck, while methods are nicer. This would really only matter if you had a function return a class that you then wanted to construct, tho, like getFooClass().new(...).otherMethod(). Right now you'd have to write that as new (getFooClass()).otherMethod(). But constructing a throwaway object in the middle of a method chain like that is pretty rare, no? If you're at the start of a method chain it's not a problem - new Foo().otherMethod() works just fine because the new binds tighter than the ..

(Note that I also think the call/construct distinction JS uses is pretty dumb and it would be great if we weren't saddled by it. But we are, and we have to live with that.)

3 Likes

yes, it can't be used to disambiguate intents and proxy traps between Util() and new Class() because new doesn't exist in foreign PLs but calling Class() only invokes the wrong trap and in JS there's no native way to understand a function should always be invoked as class instead, because that's what any class declaration enforces too.

to circumvent tihs issues interpreters do:

  • consider Class.new(...) approach when familiar with the used PL too (Ruby)
  • try hard at not failing when the PL accepts Class() but the JS would fail there (Python)
  • use awkward, home made, indirections (see PythonMonkey and Lua - Wasmoon)

this list will only grow over time, branching out per each interpreter that "bastardizes" JS classes instantiation.

That means polluting Function.prototype because all these bindings do is trap, via proxies, namespaces and any prop accessed behaves accordingly with users' code/indent, can't be AOT.

That "pretending" becomes a Function.prototype.new pollution that, if done in a worker, is not nearly as bad as done in the main, but most WASM with JS (DOM) bindings work in the main.

not outside JS ... I do this for living, I kinda deal with this issue every day, 8 hours a day and I see this growing in branching and shenanigans and the result is that every other PL blames JS, as that's easy, because JS doesn't offer a way to explicit new Class in their favorite language.

Most methods in native proptotypes are "just ergonomics" and I'd love to see new in Function.prototype or I'll add and promote a global proto pollution myself because people working cross languages will hit this wall sooner or later and it's annoying.

That's not the only way to do it, correct? Could they have also designed it to be a proxy that passes through all operations to JavaScript with the exception of .new(), which behaves specially and they let that pass through to new ...?

You do run into the potential problem of "what do you do if the function already has a member on it named 'new'", but by trying to introduce a native Function.prototype.new property, you're sort-of already assuming that people are generally not doing this, or we'd have problems getting this feature out.

We've been talking specifically about Python's here - I'm curious about how other languages are handling this issue? Is the scenario as bad for them?

Don't interpret the above as a "no", think of it instead as a "but what about..." :).

They can’t, because there’s no native JS utility to understand if a class requires mandatory new so having a new out of a function, which also requires extra checks on their side for the type of the target and eventually branch out of an already inevitably complicated and convoluted mem leaks safe orchestration, would also promote the second version of the proposal with self bound class, potentially conflicting with other new implementations that follow the first example. If this was standard instead, none of this would be an issue.

About others having new as function/class method/field, I can’t think of any different usage and never had that code in front of me.

I'm confused; don't you have the same problem identifying which functions are constructible with new vs .new()? You still wouldn't be able to construct a non-constructible function, or call a non-callable one.

1 Like

new is just a trap, like anything else, there’s no need to disambiguate or guess intents and logic … it works seamlessly like Reflect.construct would … there’s nothing special about methods or fields, they are all treated the same. If internally there is a new (syntax) call or an explicit Reflect.construct these will work as usual without issues. Nothing different in userland, nothing to guess, nothing to branch, and if a Symbol or anything non constructable happens, an error is both thrown and expected.

While no one should be modifying function prototype to add their own shared new method, it's not entirely unheard of to have static new methods defined in user classes (example). Even if a native new method was added (and same would apply to a proxied new), there's still going to be a potential conflict with definitions like these.

This sounds good. Python/Rust/Kotlin etc can pretend all JS functions have a .new method even if it's not there in reality. If the object says it HasProperty("new") it will call that method like normal, other wise Construct the target instead.

Not at all, that’s how overriding works in OOP since about ever … there’s no conflict in that example, that static method will do what it has always done to date.

It does cause issues. It's why the groupBy proposal had to opt for using the static Object.groupBy() methods. Originally they wanted to use yourArray.groupBy(), which would have been more ideal, but found that that would break some websites out there who modified Array.prototype to have a groupBy method, so they instead tries yourArray.group(), but that had issues as well, so they finally are settling on a static method.

This isn't to say that, going forwards, TC39 will always use static methods, but static methods are less likely to run into conflicts - prototype method names aren't always going to be available for use.

Personally, I'd like to see Function.prototype get treated more like the way they treat Object.prototype, and we avoid adding new protytype methods to it altogether. It's very useful to be able to design a library that exports callable objects with other arbitrary methods tacked on it (think of the "it"/"test" functions exports by testing libraries, that are callable by themselves, but could also have other methods on them, like "it.only()"), But designing APIs this way could be problematic if TC39 were to come out with a new native method, like "anyFunction.only()", that has different semantics from your library's "it.only()". Even if it might not cause a literal breaking change (though sometimes it can), it still very confusing to code readers who see a ".only()" in a codebase and it doesn't do what they think it does because the original meaning is being masked by the library's version of the function.

Though, I know that my view isn't shared by many (or any?) delegates. Otherwise this discussion would have been a lot quicker.

No it doesn't. That's an explicit static method, it's not an example of Function.prototype.new pollution. That's a class PPOM { static new() {} } and it won't have any issue.

That means that's safe to extend to me, because it'll never conflict or be hostile thanks to TC39 decision to never extend that.

Nobody can provide Function APIs ... Function is not extensible due CSP and the fact super() can't be called unless you use workarounds.

if that's a Function method, they already extended the Function.prototype so it's on them ... but again, we're discussing new here, which is a keyword, with an exact meaning, that if already extended, would likely do exactly what's being proposed in here.

In short:

  • Function is not ike Object ... super() in Function does a completely different thing that's CSP hostile and it has no meaning for the purpose of extending the current function (see the mentioned workaround, it never invokes super, it never invokes the function in initialization as Function class extend)
  • static methods explicit on classes won't be affected
  • any non static method already inevitably landed in the prototype
  • I'd like to see concrete breaking examples, not examples that won't be affected by this proposal

If the resolution here is that TC39 will never extend Function then my resolution is that it's safe, and not language hostile, to pollute the Function prototype and extending it requires community alignment and not TC39 involvement.

That works for me.

To be clear, I’m not aware of any consensus for not extending Function.prototype nor pushback to doing so, and i advise continuing to treat all objects you don’t own as unsafe to modify (modulo a spec-compliant polyfill, ofc).

1 Like