Function.prototype.new

it's the entirety of the global scope + any 3rd party library that lands on that scope ... I've no idea what you are talking about and it's clear you have no experience with foreign WASM targeting PL to me, you wouldn't be so naive in suggesting transpilers, try catches all over, and undesired invokes.

Once again: what is the issue to not land something that, as shown by examples, and reasoned by real-world issues and use cases, would make any of your suggestions completely not-needed and all the code around would simply flow as naturally as possible within foreign PLs? It won't change anything for JS developers but it will change interoperability for good ... why is this so difficult to land?

None of the proposed solutions is reasonable, fast enough, or bullet proof, the proposal of .new solves all the problems at once.

to clarify further the nonsense of this idea, if I got it right, there's no way to disambiguate function util() {} ... it can return any desired primitives but if constructed it will return an instance of itself ... you know this stuff, I don't understand how can you suggest these sloppy and bad hacks around.

I can't use Reflect.construct(util, args) and do anything meaningful with the result if that doesn't throw.

In your function util() {} example, you simply can't know if it's appropriate to use new with it without a human reading the documentation. There's simply no way around that.

I'm certainly unfamiliar with running "not JS" in a JS environment, but that sometimes some things that people want to do are simply impossible, and it's sounding like this is one of them.

exactly my point ... so Pyodide can't use Thing(), which is the pythonic way of instantiating an instance from a class, hence it needs to disambiguate with new.

Now, as shown, new as magic accessor doesn't work out of the box ... it needs various checks and it can't be considered a function (can't really meaningfully bind it neither) ... it's a hack/workaround decided by Pyodide team to help users make it explicit when they want to create an instance instead, and it currently throws right away with scenarios already presented, and even if my filed PR gets fixed, it will never be bullet proof or robust as much as having it backed in the JS specs would.

With Function.prototype.new there's no need to:

  • use transpilers (this won't work regardless)
  • use try/catches all over (this can de-opt performance)
  • use Weak references to flag constructable VS not constructable ... it's already backed in the Proxy trap
  • guess what's the intent ... it's explicit in specs
  • pretend any class has a new ... it requires more robust checks
  • branch logic, syntax, or utilities across all interpreters ... there is one way to create an instance procedurally ... no reason to re-invent the wheel
  • there are proxy traps that just work out of the box for all the PLs that will land on the Web and don't have new or similar syntax to instantiate objects

.... but yeah, daily JS developres would care very little, even if I still think there's value in being able to Class.new.bind(Class) in the wild, which will allow Class() or new Class for map use cases (list.map(Class.new, Class)), and what's not ... it'd be tiny extra thing, but it surely won't harm the JS community, while it can make life easier to everything coming from foreign programming languages.

The alternative is to ask them to use the polyfill and forget about this thread ... which I will start doing at some point, as the poly fits in a tweet and it solves so much it'd be a bit stubborn to decide to not use it ... after all, it's clear in this thread Function.prototype.new will never exist, so it's safe to bring in one to me.

Here it's the other way around ... it's JS running in Python environment (or Lua, or Ruby, or go, or ...) ... these interpreters bring real engines that is not the JS one to the plate (C-Python, Ruby-wasm-wasi, and so on), which is why transpiling is not an option ... it's not JS that gets executed, it's other PLs.

Why would Foo.new() be any different than new Foo()?? They’ll both work, or throw, in the same circumstances.

I may be wrong, but it seems the real different of opinion across this thread is how 'bad' it is that these other languages have an extra block of logic in their representation of JS objects. Just because they are "pretending" that functions have a .new property that isn't necessarily a bad thing. The examples of how this can 'break' are 'fixed' in the examples that only override .new if it's not present.

get(target name) {
  const v = target[name];
  if (v === undefined && name === "new" && typeof target === "function") {
    return (...args) => new target(...args); // or traps.construct if required
  }
  return v;
}

While this adds "complexity" to these libraries, it is a small number of lines to add. Perhaps this is OK? I do see that there is an amount of value in standardising this approach but all changes to a language have a non-zero cost. So it has to be balanced against that cost. I'm not saying which is greater, the value or the cost. I am saying that it is to be expected to spend time discussing both sides. EcmaScript is a language that, generally speaking, can only ever add things, which means that a decent amount of time is spent discussing this to be as certain as possible that it is the right decision given the information available at the time; because once it is added it will likely be there forever.

On the 'cost' side of things. Adding .new as part of ECMAScript means there is now another way to construct classes for JS developers - will this create style debates on using C.new vs new C ? There is some, again subjective, about of value in not having a multiple ways of doing the same thing in a language. Maybe this is one of those places.

what throws with new in JS should throw with Class.new() ... I don't understand the question. Symbol.new() should throw like arrow.new() would, unless they have different new defined explicitly that does something else.

speculative but one guards around syntax, the other one might intentionally not guard and make instances observable in construction, just like Reflect.construct(Class, [...args]) could allow ... so it's more about intents or desired features one or the other way allows.

Your fix creates potentially dozen, if not hundreds or thousands, of one-off arrow functions, potentially polluting .length or .name expectations of the new native function, so we have more GC operations either via one-off creation or WeakRef keys that returns one and one only function per each Class.new.

With inheritance, it will always be a single borrowed function per each invoke, and there's nothing to handle from all the PLs that use bindings and all these PLs don't need to be sure they're using the right checks, the right name, and the right workaround ... if they all do though, what's the point of not making that workaround standard? So it becomes a community effort to align, and my answer to that is the @ungap/new polyfill to rule them all and drop any of these issues out of the box.

P.S. your check fails my example with the arrow which has an own new property too ... that's what we're discussing: everyone showing examples managed to fail in a way or another to produce robust code. edit actually my bad ... it checks VS undefined ... although the name === 'new' check should be earlier, because target[name] could have side-effects, so it's still really not bullet proof or perfect that way.

Wouldn't the same happen with all Python hosts either way? Python has auto-binding methods. So I would expect all getters that return function to .bind them creating new objects. To reduce allocating the function more than once per class a WeakMap can be used:

const constructorCache = new WeakMap();
...
get(target name) {
  const v = target[name];
  if (v === undefined && name === "new" && typeof target === "function") {
    let f = constructorCache.get(target);
    if (!f) {
      f = (...args) => new target(...args); // or traps.construct if required
      constructorCache.set(target, f);
    }
    return f;
  }
  return v;
}

Fails how?

I've edited as it doesn't fail indeed my arrow example but:

  • the name === 'new' check should be done earlier, and the typeof target === 'function' should follow
  • if done too late as you did, there might be undesired side-effects in target[name] if that's an accessor
  • once first checks pass through, you need to disambiguate name in target to avoid accessing it due previous point
  • once that's done, you are self binding the operation ... which is OK, but you need to add GC pressure due WeakMap that retains any possible Class.new ... WeakMap is slow in setting but at least fast enough in reading ... still, performance are penalized
  • there are surely other weird cases that I am not considering but the gist is that ...

... none of this would be needed if we had Function.prototype.new backed in.

it's actually (...args) => this.construct(target, args, receiver) to have the best approach that can forward the proxy which potentially has other stuff attached by other WeakMap (or it can be ignored) and there's a single entry point to orchestrate new proxies for the returned objects/references.

See how this "simple to solve in uerland" can actually have tons of evil details in it without a standard defined approach that just works with Proxies too? This screams for a library that does the right thing for them all ... at that point I rather just suggest @ungap/new.

The reason I am checking later is so the synthesised .new property is only added if it is not there (or is there but undefined). The function doesn't need to bind, it could return the same as Function.prototype.new would.

function functionNew(...args) {
  return new this(...args);
}
...
get(target name) {
  const v = target[name];
  if (v === undefined && name === "new" && typeof target === "function") {
    return functionNew;
  }
  return v;
}

This should achieve similar semantics to those that would be present with Function.prototype.new. I do agree there are caveats with this, and it is possible to write code where this approach would not be as 'robust', are those cases likely in practice?

I like the fact that your solution is my proposal ... I still see potential issues in accessing target[name] and check after though, because of the presented override case:

class Factory {
  static new(...outer) {
    const scoped = complexOperation(...outer);
    return (...inner) => scoped.doStuff(...inner);
  }
}

I have tons of factory-like classes or utilities that return functions ... your code solves the WeakMap issue, but it won't solve possible explicit utilities ... and bear in mind my class Factory could instead be a magic factory utility that works both as callback and through its special new field ... it returns a function that will satisfy that proxy trap and once again we will have hard to understand, debug, or reason about, bugs / discrepancies between JS and Python or any other PL that targets WASM.

... have I mentioned already "none of this would happen if Function.prototype.new was backed in the language?"

My previous examples, with my polyfill in, shows that nothing we're discussing in here is needed or fails ... the factory example works, and so would any other, without caveats, without manual checks, without side-effects ... it would just work out of Proxy traps: that's what I call robust code and I'd love to be able to write and use robust code in my projects!

edit on a second thought ... it is possible that your get trap would pass the check but it could fail if a new explicit method or accessor returns undefined ... yeah, in that regard this check might be at least a bit more robust, still not ideal.

To whom it might concern, I've published an @ungap/new alternative that doesn't land in the Function.prototype, addressing almost any possible comment and workaround proposed.

This module is neither ideal nor perfect, but it covers edge cases discussed in here in a way that plays nicely with generic proxies orchestration, without accessing the property at all so it plays better also with side-effects and getters.

Maybe using this around would prove that this proposal is more than welcome and ideal to have instead of yet another module in the JS world, but time will tell, I still think this proposal holds a lot of value for the present and the future of both JS and WASM targeting PLs.

1 Like

As there's no progress in here and I don't expect any at this point, I'd like to summarize the outcome of this discussion, partially as summary, partially to end this thread with something worth everyone time in it and possible users reading, in the future, the conclusion.

  • it is documented that there is a need for better interoperability between JS and WASM targeting foreign PLs, as these might not be able to parse new Class in the wild and within their foreign PL syntax ... the proposal is to make Class.new(...args) a standard/expected behavior, to solve all issues
  • it is unlikely that this proposal will ever land in ECMAScript, as pretty much everyone opposed to the idea that Function.prototype.new should be in JS just because many other PLs can't deal with JS syntax in their foreign PL
  • it is acknowledged that such proposal would solve the issue without caveats, but alternative solutions have been presented to help these foreign WASM targeting PLs to solve the issue by their own
  • the most robust proposed alternative is that every WASM targeting PL should use a Proxy get trap to figure out if new is desired, if it's not already present in the target, and when that's the case, return what's being proposed in here in the first place, but as workaround out of suggested checks, so that it's possible for these PLs to use Class.new(...args) without needing ECMAScript changes to help them
  • accordingly, it's acknowledged that Function.prototype.new can't ever exist in JS, otherwise these WASM targeting foreign PLs would suddenly break out of the blue, in case a new method lands in Function.prototype and it doesn't do exactly what's being proposed in here ... so Function.prototype.new in this thread has been granted/sealed, and locked in, forever as non-existent, because after knowing WASM targeting PLs will use what's been suggested in here, it would be pretty evil to land in there anything else that's slightly different from this proposal (Python is also heavily used at academic level: changing JS means changing every student or professor code based on WASM and JS bindings expectations)
  • in turns, this means that both polyfill to have Function.prototype.new defined, or consider there won't ever be an inherited Function.prototype.new in JS, are both valid solutions in the short to long term (the polyfill is not future hostile, the module trust no Function.prototype.new will ever exist out of specs or ... if it does ... it's exactly the proposed one in here)

Please feel free to comment any of these points so that I can update eventually the summary but I believe the scafolded logic is sound, as result of this thread.

Regards.

That may be your outcome, but many of those bullets aren’t something i think others agree with.

I still don’t see how this proposal solves anything, and modifying objects you don’t own is never ok - changing the results of reflecting on builtins can cause problems too.

1 Like

as you confirmed, you don't have experience with this kind of interoperability issues so please forgive me if I'll stop answering your questions, as not relevant for the thread to me.

about agreeing though, I've summarized the logical consequence of this thread where NO was the answer and workarounds were suggested ... if we follow those workarounds, you gotta take responsibility for providing a NO and suggest workarounds that make that NO more troublesome than a Yes would've been, in the long run.

we're following advices here, and those advices were loud and clear: "it's on you all, just use this not-super-reliable workaround and be good with it". That's in every "just Pyodide can (bug filed as it can't) + just use this trap everywhere" answer out of this thread.

edit in case it's not clear, Class.new(...args) is inevitable. Here I was hoping for the least friction approach around it. If you think "what does it solve?" you are not understanding that other programming languages exist and are landing on the Web. If you understand that instead, you'll also understand that new Class(...args) is not necessarily an option in other programming languages. If you can't see why, I am afraid you need to get out of your JS bubble to understand why. If you think you got it though, you should put yourself in the shoes of all the non JS students or people in the world that need to write pm.Reflect.construct(pm.nmsp.Class, [...args]) or pm.new(pm.nmsp.Class)(...args) instead of sitting confy on their couch with new nmsp.Class(...args) like you do!

I still don't understand why the following is not an acceptable DX:

pm.new(pm.SomeClass, ...args)

// or

// One time "bind"
NewSomeClass = pm.makeNew(pm.SomeClass)
// then other code
NewSomeClass(...args)

Also all this is assuming dynamic PLs? I'm not sure how the arg list would be handled in a statically typed programming language, or how JS functions and objects are typed in the first place.

for the same reason nobody writes Reflect.construct(Class, args) instead of new Class(...args) and the same reason nobody writes const newClass = Reflect.construct.bind(Reflect, Class).

It's extremely awkward to both read and write and Class.new(...args) is an elegant, desired and already used (it's been years) solution to this problem, and I am asking other PLs to implement the same so that we'll be all aligned and there's less to worry about in the future.

We have both students and companies relying on Pyodide Class.new convention, we have Ruby already working natively like that, and we're discussing with both Wasmoon and PythonMonkey to provide the same for community sake and to seal the deal around the pattern.

If not having new Class is not acceptable for JS developers, it takes little effort to wonder "how come others would like that simple way too" ... and Class.new is the answer to that question.

It's completely out of scope but if static languages lack new Class syntax, they can transform Class.new(1, 2, 3) ... the ...args there is to describe the behavior/signature, not to demand dynamic arguments everywhere.

Likely my last shot on this thread, explaining why the goal of this proposal is to improve interoperability ...

We have already Pyodide and MicroPython working out of the box, meaning one can write the following code and be sure that when, and if, the interpreter changes, everything will still work as expected:

import js

js.document.body.textContent = js.Date.new().toISOString()

We can provide, if needed, modules that normalize access to the global context (that is import js that points at import pm), but when it comes to PythonMonkey, that code will break/fail because it doesn't use the same convention.

Theoretically though, that code could work as is also in other PLs, but that's a stretch ... what I am after here, is the fact interoperability is a very well understood problem in NodeJS vs bun vs deno vs gjs vs thingamabob (aka: any other runtime) but somehow nobody understands this argument in here ... this proposal would like to seal the deal around JS bindings and instantiation of classes, as it's a nightmare to explain, document, or even understand, everyone doing something slightly different out there to obtain the exact same result.

We don't need to explain much that if Python (or others) don't understand new Class keyword, or it's reserved, the way to do the same is to use Class.new(...) instead, and the code that could run in various interpreters with JS bindings in it, will keep working, as long as the code doesn't use/have specific interpreters gotchas in it ... but that's not on us, all we're trying to do is to ensure our users that they can swap runtimes when they feel like it's needed (MicroPython takes few ms to boostrap, Pyodide takes up to 2 seconds or more, if there are foreign packages to also fetch and orchestrate) ... so that as a user, I'd love to write code that works and when I need more from the interpreter just change such interpreter (we also allow Workers to bootstrap different interpreters for the same PL behind the scene, so ... there's that too!).

This is the background around this request, this is obvious to me daily, but I hope now that's written down in words becomes obvious to you too that Class.new(...) already exists and it will keep existing, so it'd be great to consolidate that pattern or it's chaos out there, and TC39 is partially responsible for it if there's no action following this request.