Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Built-in extension functions #17

Open
andyearnshaw opened this issue Sep 26, 2024 · 5 comments
Open

Built-in extension functions #17

andyearnshaw opened this issue Sep 26, 2024 · 5 comments

Comments

@andyearnshaw
Copy link

I'm a big fan of the idea of extension functdions and accessors. I even posited the idea for virtual accessors back on the old bind operator proposal, so I'm glad to see they made it into this one. Every time I use Kotlin for something, it reminds me to check up on how this proposal is doing! It looks like there's been a bit of a stall, but I'm hopeful it will move forward at some point.

One thing I think might help is if there are some built-ins to go along with the concept in the proposal. These could help demonstrate the usefulness and expressiveness of extensions. For example, Kotlin has a few that are designed for avoiding creating temporary variables. Taking a leaf from that book, here are a few that could be built-in for JavaScript.

apply

Calls a function with the context passed as the first argument, always returns the context. This would be your most basic extension function that you use when you don't want to create one for such a simple task.

mySet
  .add("one")
  .add("two")
  .add("three")
  ::apply(it => console.log(`So far, we have ${[...it.join()]}`))
  .add("four")

let

Similar to apply, but returns the result of the provided function rather than the context. Pairs well with optional chaining to avoid if checks for nullish objects:

const update = (selector, status, text) =>
  document.querySelector(selector)?::let(it => {
    it.textContent = text;
    it.classList.add(`status-${status}`);
    return customElements.whenDefined(it.tagName).then(() => it);
  }) ?? Promise.reject();

// Equivalent to
const update = (selector, status, text) => {
  const it = document.querySelector(selector);

  if (it) {
    it.textContent = text;
    it.classList.add(`status-${status}`);
    return customElements.whenDefined(it.tagName).then(() => it);
  }
  else {
    return Promise.reject();
  }
}

takeIf

Returns the context only if the predicate function returns true, else returns undefined.

const getProduct = (label, description, quantity) => ({
  label,
  description,
  quantity: parseInt(quantity, 10)::takeIf(it -> Number.isFinite(it)) ?? 1
});

// Equivalent to
const getProduct = (label, description, quantity) => {
  const parsedQuantity = parseInt(quantity, 10);

  return {
    label,
    description,
    quantity: Number.isFinite(parsedQuantity) ? parsedQuantity : 1
  }
};

WeakSymbol()

Creates an accessor that lets you attach data weakly to an object.

const ::data = WeakSymbol();

document.getElementById('myDiv')::data = 'foo';
console.log(document.getElementById('myDiv')::data);
// -> foo

This would be syntactic sugar for, roughly (according to the current proposal), the following:

const ::data = (() => {
    const wm = new WeakMap();

    return {
        get() { wm.get(this) },
        set(v) { wm.set(this, v) }
    };
})();

However, it would also mirror the existing Symbol() constructor, with WeakSymbol.for(key) and WeakSymbool.keyFor(::extension).


That's it for now. I'll update this if I think of any more, and please feel free to chip in ideas of your own.

@hax
Copy link
Member

hax commented Sep 26, 2024

Thank you for your suggestion. These are excellent examples of extension methods. Although these can all be implemented in user-land, so there's no urgency to include them in the proposal, and as I understand the tc39 proposal process, these features could be considered for future proposals. However, I agree with your idea that we need more examples like these to demonstrate the potential capabilities of extensions. I'd like to add them to my slides and present them in the future tc39 plenary.

It looks like there's been a bit of a stall

I am currently refining the proposal and hope to update and advance it at next year's meeting. My current work focus on the core semantics and syntax. For example, the syntax of the proposal will change. The original proposal followed the bind operator's :: symbol, but considering the .#foo syntax used by private methods/accessors that have reached stage 4, it might be better to unify extension methods/accessors under a dot-based syntax. Secondly, the semantics of the proposal will also be simplified. Nevertheless, I believe the core spirit remains unchanged and will still meet all the use cases presented here.

Although I do not plan to include built-in extension methods in the current proposal, there are indeed some issues that need to be considered in advance. Since the proposal is shifting to a dot-based syntax, the initial ad-hoc definitions and binary forms (such as const ::data = WeakSymbol(); obj::data = ...;) may have to be abandoned. Theoretically, we could switch to something like const @data = WeakSymbol(); obj.@data = ...;, but we have almost no appropriate sigils left, and when this proposal entered stage 1, some delegates strongly opposed the introduction of additional namespace. This means that built-in extension methods must use explicit namespaces, which would be the initial ternary form, such as const extra = WeakSymbolContainer(); obj.extra:data = ...;. A subsequent issue is where methods like let and apply should be placed if they are to become built-in methods. Function might be a natural choice, but value.Function:apply() does seem a bit too verbose. One possible choice is to use existing keywords as built-in extension namespaces; the most suitable might be do -- value.do:apply(...) looks quite good.

@Jack-Works
Copy link
Member

A subsequent issue is where methods like let and apply should be placed if they are to become built-in methods. Function might be a natural choice, but value.Function:apply() does seem a bit too verbose.

It's possible to do this: for code value:identifier, find identifier in the current lexical scope first, then if there is none, find it on the Function[indetifier].

value:takeIf() // call Function.takeIf
{
    const takeIf = () => ...
    value:takeIf() // call the function above
}

@hax
Copy link
Member

hax commented Oct 9, 2024

@Jack-Works We can't use x:foo syntax directly, it already a thing (labeled statement) today. And as we are moving to dot-based syntax, obviously we need some syntax diff from x.foo, so the most near is something like x.@foo (private already take x.#foo). And the first line should call globalThis.takeIf (not Function.takeIf) to be consistent.

But as I presented in stage 1 slides, it's really a footgun that const json = value.@json() can't work (and also shadow footgun), espeically normal methods/private methods do not have such issues.

@zaygraveyard
Copy link

I feel @ should be reserved to decorators, so what about x.:foo?
Can labels contain a .?

@hax
Copy link
Member

hax commented Oct 9, 2024

I feel @ should be reserved to decorators, so what about x.:foo?

JS already used almost all symbols, so some symbols are overloaded. For example, # is used for private names, but also used for hashbang, tuple&record, and maybe other proposals if there is no syntax conflict. @ is also same. Actually there are issues suggest to restrict the syntax of decorators to allow @ could be easier to use in other places (eg. tc39/proposal-decorators#430 (comment) ).

Can labels contain a .?

No, label must be a valid identifier. x.:foo is a possible syntax. But the issue is not if we use .@ or .: but the last paragraph. (How to make const json = value.@json() work.)

Anyway I think we could postpone this part, let's first focus on x.ns❓foo syntax and semantic. ( is the delimiter to be bikeshed, see #16 (comment) )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants