Reshaping the Extensions proposal

Proposal: GitHub - tc39/proposal-extensions: Extensions proposal for ECMAScript

I'd like to suggest reshaping the Extensions proposal, pretty much like @hax did before in tc39/proposal-bind-operator#56.

There's been no updates to the proposal for more than two years, despite the fact that it could help a lot of developers with real-world use cases.

I'd like to suggest an alternative to what's been worked on. Essentially, it involves reducing the scope furthermore and make all functions usable as extension functions without any new syntax or operator, something like that:

function countWords() {
  return this.split(/\s/).length;
}

// Any function within the current scope can be called with a receiver:

"Hello, World!".countWords();
// Returns: 2

I already have written a specification of this reshape: GitHub - nesk/proposal-extension-functions

I'm not sure how to go further since a similar proposal exists?

This would not be compatible with existing code. Which is why the exciting proposals introduce new syntax. That also reduces the runtime overhead, to not impact every single property look up, which can dominate program execution time.

1 Like

Also… welcome! :waving_hand:t2:

1 Like

I'm not sure how, do you have any example in mind?

This is the part I'm not really an expert in but, from what I understand, JavaScript is making the following operations when executing receiver.sayHello():

  1. Looks up for a receiver variable in the scopes, finds it.
  2. Looks up for a sayHello property in the object's own properties, doesn't find it.
  3. Looks up for a sayHello method in the prototype, finds it.

Now suppose sayHello isn't a method on the object prototype but rather an extension function in the current scope:

  1. Looks up for a receiver variable in the scopes, finds it.
  2. Looks up for a sayHello property in the object's own properties, doesn't find it.
  3. Looks up for a sayHello method in the prototype, doesn't find it.
  4. Looks up for a sayHello variable in the scopes, finds it.

Am I right? If so, is this really something we can call a runtime overhead since it's only one additional step pretty similar to the other ones the JS engine is already used to?

so since globalThis is an object, and globalThis.toString is Object.prototype.toString, suddenly every null object would suddenly start reporting true for 'toString' in obj or returning "not undefined" for obj['toString'] or obj.toString? That would break a lot of things.

The repo has some more information.

Says that only module scope is considered, not the global scope.

And the “extension” is only called when the object doesn’t have that property, which might lead to a surprising situation when a method is added to an object that than stops the extension method being called. Which is maybe another reason to be explicit? Other languages with extensions methods, like Kotlin and Swift. Are statically typed, so it's easier to spot when this will happen. More surprising in a dynamic language.

No, read their proposal - it says that solely the obj.method() syntax ends up looking at the surrounding scope, nothing else. obj[“method"]()doesn’t work, etc.

2 Likes

Then that's another problem, because only string identifier-named methods would work, which precludes both symbols and any string that's not an identifier. Every property has to be extensionable, or none can, imo.

I agree this could be an issue, I would be glad to discuss this furthermore.

Like I said in my first message, the goal of this reshaping is to reduce the scope, which allows to reuse an already existing syntax for extension functions.

By limiting the usage of the extension functions to specific use cases, we can avoid a lot of incompatibilites, this is what I tried here.

The behavior you're talking about would still be possible, but not as friendly as string identifiers:

const iterator = Array.prototype[Symbol.iterator];
const array = ["a", "b", "c"];

array.iterator().next(); // Returns: "a"

There is also:

function bar() {}

object.bar?.()

Would this call the extension, or is this opt-ing out of the extension lookup?

Overall my feeling is that overloading existing syntax doesn’t provide good experience for this. As the language is dynamic you can’t easily know when reading that code if the extension will be used. I personally prefer either the :: or the |> pipeline explicit approaches.

2 Likes

Totally missed that and it's a good point. Since you're in control of your scope and what extension functions are available to you, I would say your code is opt-ing out of the extension lookup because there is no reason the function would be missing.

I will add mentions about optional chaining to the proposal.

Correct me if I'm wrong but from what I read from multiple meeting notes, TC39 committee seems to want to limit new syntax additions to the specification?

The committee is a collections of many different voices. Personally I am ok with new syntax if the value add is justified.

For the delegates who are much more hesitant about new syntax, their reasons may extend to this idea. Even though this looks like existing code it’s still driven by new syntactic semantics. You need to know the AST to implement this.

3 Likes

This is why the feature is scoped to modules, to avoid too much context and help understanding if you're calling a method or an extension function.

Also, your IDE should be able to help furthermore.

Even if the extension lookup is scoped to the current module, the harder part is knowing if the object already has that property or not. For example

function ext() {}

export function foo(v) {
   return v.ext();
}

It depends what is passed into the function to know if the extension will be used.

Even with TypeScript enabled while editing you can't be sure.

function ext() {}

export function foo(v: { name: string }) {
   return v.ext();
}

The type { name: string } does not mean that the object does not have a ext property. The type is not closed. The type means the object should have at least the name property.

1 Like

That's a good point too. Maybe taking inspiration from non-scripting languages isn't the right thing to do.

The reason these languages forbid overriding is—partly—because you can't rename the extension functions and it would mean you could get stuck with an extension function shadowing the original method. But since in JS you can simply alias an import, the rule might not be interesting to apply here.

What would you think of extensions always overriding class and object properties? This would allow to always have a deterministic execution.

Without overriding (before):

function toString() {
  return "<REDACTED>";
}

const password = "5iadCWgh";
password.toString();
// Returns: "5iadCWgh"

With overriding (after):

function toString() {
  return "<REDACTED>";
}

const password = "5iadCWgh";
password.toString();
// Returns: "<REDACTED>"

The 'extension' always taking precedent is what I thought you were originally suggesting in my first post (I had rushed into replying without reading your linked repo :man_facepalming:).

Always applying the extension without new syntax would be a breaking change for existing code that isn't web compatiable.

Modules aren't the only parse goal JS has - a feature (that's not inherently tied to modules) being limited to Module is a nonstarter; it must work in Scripts.

You're right, I totally forgot about that :sweat_smile:

You've mentioned a new syntax multiple times, do you think this would be appropriate here?

If we add a new syntax, we would be able to make sure the extension always takes precedence without any breaking changes.

Yes, this is why I mentioned it in my proposal. Limiting the extension functions to modules isn't necessary after all, it was just to help reduce the scope breadth. The important part is forbidding the usage of the global scope.

Since we've established that new syntax is pretty much mandatory for solving this problem, is there any reason to go for syntax that's narrowly focused on this problem (e.g. an "apply" operator like ::) vs a slightly more general syntax addition like "pipeline"?

1 Like

I think this proposal brings a bit too much magic around to solve 4 chars (as in `.call`) which would be the explicit way … so, for what is worth it, I agree a new explicit syntax would be much better

1 Like