tc39 / proposal-extractors

Extractors for ECMAScript
http://tc39.es/proposal-extractors/
MIT License
200 stars 3 forks source link

Problem statement #18

Open codehag opened 4 months ago

codehag commented 4 months ago

Some questions came up around the motivation, in particular the current wording:

"ECMAScript currently has no mechanism for executing user-defined logic during destructuring"

From reading the proposal, it doesn't seem that arbitrary user logic during destructuring is the goal. In a sense, getters do this as well. The proposal rather seems to enable user customized destructuring? or something to this effect? Maybe I am wrong here?

I see some overlap with the motivation of pattern matching:

"There are many ways to match values in the language, but there are no ways to match patterns beyond regular expressions for strings. However, wanting to take different actions based on patterns in a given value is a very common desire: do X if the value has a foo property, do Y if it contains three or more items, etc."

This proposal seems to allow users to customize the destructuring of an object according to a pattern that the user defines?

rbuckton commented 4 months ago

Some questions came up around the motivation, in particular the current wording:

"ECMAScript currently has no mechanism for executing user-defined logic during destructuring"

From reading the proposal, it doesn't seem that arbitrary user logic during destructuring is the goal. In a sense, getters do this as well. The proposal rather seems to enable user customized destructuring? or something to this effect? Maybe I am wrong here?

You are correct. It's the ability to evaluate code that affects destructuring of an input. Getters only affect how an object produces an output for a given property.

I see some overlap with the motivation of pattern matching:

"There are many ways to match values in the language, but there are no ways to match patterns beyond regular expressions for strings. However, wanting to take different actions based on patterns in a given value is a very common desire: do X if the value has a foo property, do Y if it contains three or more items, etc."

This proposal is closely aligned with Pattern Matching, as extractors will follow the same semantics in both cases.

This proposal seems to allow users to customize the destructuring of an object according to a pattern that the user defines?

Extractors are synonymous with custom matchers in the pattern matching proposal. The majority use case for extractors will be to define a mechanism to validate the subject (i.e., the RHS of the assignment, or the match subject) and invert construction to acquire the inputs, i.e.:

const p = new Point(1, 2);
const Point(x, y) = p;
x; // 1
y; // 2

Extractors fill a capability gap in both destructuring and the pattern matching proposal, but can't serve as a pattern matching mechanism solely on their own.

codehag commented 4 months ago

Great, I think if we can tighten the language around the motivation, it might be clearer to people who see it for the first time.

rbuckton commented 4 months ago

How does this sound:

ECMAScript currently has no mechanism for executing user-defined logic as part of destructuring, which means that operations related to data validation and transformation may require multiple statements:

We're not exactly customizing destructuring, as {} and [] operate per usual. This wording takes the focus off of "all potential user-defined logic" that can execute (computed property names, getters, setters) with a focus on injecting user code into the destructuring process itself.

eemeli commented 4 months ago

Maybe I'm missing something, but starting from the example in https://github.com/tc39/proposal-extractors/issues/15#issuecomment-2037675151, wouldn't something like this be much simpler and already possible?

let x1 = line.p1.x;
let y1 = line.p1.y;
let x2 = line.p2.x;
let y2 = line.p2.y;

and if it's a concern that we're accessing the same property of line more than once, all of that is of course possible with destructuring and aliasing:

let { p1: { x: x1, y: y1 }, p2: { x: x2, y: y2 } } = line;

presuming, of course, that the Point has x and y accessors. Which it must have in some manner for the extractor to work, yes?

So why are we introducing a more complicated way of doing a thing that's already possible? Is there something that extractors make possible that isn't currently possible?

rbuckton commented 4 months ago

So why are we introducing a more complicated way of doing a thing that's already possible? Is there something that extractors make possible that isn't currently possible?

Extractors extend the destructuring paradigm to add support for user-defined input validation and transformation. The comment you referenced only intended to describe a semantic equivalent in current JS for a very narrow case. What is missing from that example is the implementation of Point[Symbol.customMatcher], which may be better illustrated from this example from the slides presented at the February 2024 plenary:

class Point {
    #x;
    #y;
    constructor(x, y) {
      this.#x = x;
      this.#y = y;
    }

    // NOTE: commented out to illustrate extractor flexibility in extracting
    //       private fields, while keeping the author of `Point` in control
    //       how private state can be accessed.
    //get x() { return this.#x; }
    //get y() { return this.#y; }

    static [Symbol.customMatcher](subject) {
        return #x in subject ? [subject.#x, subject.#y] : false;
    }
}

// destructuring
const Point(x, y) = new Point(1, 2);
x; // 1
y; // 2

// pattern matching
match (p) {
  when Point(let x, let y): …;
}

The [Symbol.customMatcher] method on Point is able to perform a brand check on the subject within the lexical scope of the Point class and extract the values from private fields within that class. Neither are capabilities destructuring can perform on its own.

While the same operations can be performed across multiple statements with something like a static Point.extract(p) method, such a method cannot be used in-situ in an existing BindingPattern (such as from a variable or parameter), and cannot be easily used inside of an AssignmentPattern without the gymnastics illustrated in https://github.com/tc39/proposal-extractors/issues/15#issuecomment-2037675151.

Extractors encourage a leaner coding style that is often preferred by JS developers and is regularly seen in other languages with language features like Pattern Matching:

// (assumes Point.{ x, y } are public for the purpose of this example)

const { p1, p2 } = line; // top-level destructuring
if (!(p1 instanceof Point) || !(p2 instanceof Point)) throw new Error(); // data validation
const { x: x1, y: y1 } = p1; // nested destructuring
const { x: x2, y: y2 } = p2; // nested destructuring

// vs
const { p1: Point(x1, y1), p2: Point(x2, y2) } = line;

In addition to the conciseness of Extractors in destructuring, they also allow for optimal short-circuiting in Pattern Matching:

match (shape) {
  when { p1: Point(let p1), p2: Point(let p2) }: drawLine(p1, p2);
  ...
}

Here, the brand check in Point[Symbol.customMatcher] allows the pattern to attempt to match p1 and if that fails, short-circuit to the next when clause.

Another place they shine is within parameter lists where there is no Statement context within which to split out code without shifting work to the body:

function encloseLineInRect(
  {p1: Point(x1, y1), p2: Point(x2, y2)},
  padding = defaultPadding(x1 - x2, y1 - y2)
) {
  const xMin = Math.min(x1, x2) - padding;
  const xMax = Math.max(x1, x2) + padding;
  const yMin = Math.min(y1, y2) - padding;
  const yMax = Math.max(y1, y2) + padding;
  return new Rect(xMin, yMin, xMax, yMax);
}

function defaultPadding(dx, dy) {
 // use larger padding if x or y coordinates are close
 if (Math.abs(dx) < 0.5 || Math.abs(dy) < 0.5) {
   return 2; 
 }
 return 1;
}

const rect = encloseLineInRect(line);

Here, extractors let you validate the input arguments and extract coordinates without needing to shift default argument handling for padding to the body.

Many of the above examples are more complex cases than what is generally the norm with similar syntax in Scala, Rust, C#, F#, etc., which often looks more like this:

// destructuring
const Message.Move(x, y) = msg;

// pattern matching
match (opt) {
  when Option.Some(let value): ...;
  when Option.None: ...;
}

// more pattern matching
if (opt is Option.Some(let value)) {
  console.log(value);
}

However, the extractor syntax is designed to be consistent with existing JS destructuring mechanisms as well as with the pattern matching proposal, which allows for a wide range of use cases.

codehag commented 4 months ago

ECMAScript currently has no mechanism for executing user-defined logic as part of destructuring, which means that operations related to data validation and transformation may require multiple statements:

This sounds great and resolves my concern!