proposal-extensions icon indicating copy to clipboard operation
proposal-extensions copied to clipboard

Built-in extension functions

Open andyearnshaw opened this issue 1 year ago • 5 comments

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.

andyearnshaw avatar Sep 26 '24 16:09 andyearnshaw