I started writing this in reply to #18 (comment), but realized that I was sidetracking that issue. My concerns aren't really about the problem statement, but about the scope within which a solution to the problem is desirable.
Overall, I get the value of extraction within the context of pattern matching, but not really outside that. Inside match or with an is test, the reader is primed to expect validation and, potentially, extraction. But in destructuring or elsewhere, not so much. In those other places, it comes as added complexity, and doesn't really bring a great benefit compared to current syntax patterns.
Reviewing past presentations and discussions of this proposal, it seems like I'm echoing concerns raised by @brad4d during the 2022-09 meeting:
BSH: Yeah, my main concern here is, I feel like this feature is creating a way to write code that's very terse and clever but not necessarily easier to get right or easier to understand. In several of the examples, it looks to me like, you'd have to - to really know what's going on you would have to go and read what the extractor code does to understand. Well, what things can I use this extractor with? And what can I put on the right hand side? I don't see the benefit to balance out the readability. or what, wait, what does this in some way? Let you write more efficient code easily. or does it somehow - aside from the magical feeling and cleverness, what's the benefit?
[...] So I think what would address this - Not something you can do now, but what would address my concern here, is if I could have a solid example of some code. Like, here's how you would write the code with this feature available and here's how you write it without it, and it's not just that it's shorter, but it should be actually easier to understand and maybe more performant. because, there's some benefit that you're getting performance-wise out of like the environment doing this for you. That's what I would need to see. That's all I'm saying. I'm not trying to block this or anything. I just that's that's my concern. I feel like it's just feels too clever and not really overall beneficial. That's it for me.
In other words, my concern is that the examples currently in the readme or presented in the preceding discussion don't really seem like they present the new syntax as "easier to understand and maybe more performant".
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;
On the other hand, if we were to presume an equivalent Point definition like
class Point {
#x;
#y;
constructor(x, y) {
this.#x = x;
this.#y = y;
}
static extract(subject) {
return #x in subject ? [subject.#x, subject.#y] : false;
}
}
the user code could look like
const [x1, y1] = Point.extract(line.p1);
const [x2, y2] = Point.extract(line.p2);
Sure, that doesn't do the work all on one line and during the destructuring, but the result is the same and much clearer to a reader, given that each part of the code is only doing one thing.
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.
Ok, I'll grant you that in this case we do save one access of shape.p1 and shape.p2 compared to what's possible with switch, which also requires the break:
switch (true) {
case shape.p1 instanceof Point && shape.p2 instanceof Point:
drawLine(shape.p1, shape.p2);
break;
...
}
Presuming that a Line class was being used, and with perhaps more common JS, those issues can be avoided:
if (shape instanceof Line) drawLine(shape.p1, shape.p2);
else ...
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.
On the other hand, if we were to presume that Point provided public accessors x and y, our code could look like:
function encloseLineInRect(
{ p1: { x: x1, y: y1 }, p2: { x: x2, y: y2 } },
padding = defaultPadding(x1 - x2, y1 - y2)
) {
...
}
If the author of Point did require always getting both coordinates at the same time, then it'd probably make more sense to handle the default padding in the body:
function encloseLineInRect({ p1, p2 }, padding) {
const [x1, y1] = Point.extract(p1);
const [x2, y2] = Point.extract(p2);
padding ??= defaultPadding(x1 - x2, y1 - y2);
...
}
Actually, having written out those alternatives, this last one is by far the clearest to me, as each part of it is only doing one thing, and it's much clearer that extracting the values actually involves some work.
Why should having this code in the function body be considered a bad thing?
I started writing this in reply to #18 (comment), but realized that I was sidetracking that issue. My concerns aren't really about the problem statement, but about the scope within which a solution to the problem is desirable.
Overall, I get the value of extraction within the context of pattern matching, but not really outside that. Inside
matchor with anistest, the reader is primed to expect validation and, potentially, extraction. But in destructuring or elsewhere, not so much. In those other places, it comes as added complexity, and doesn't really bring a great benefit compared to current syntax patterns.Reviewing past presentations and discussions of this proposal, it seems like I'm echoing concerns raised by @brad4d during the 2022-09 meeting:
In other words, my concern is that the examples currently in the readme or presented in the preceding discussion don't really seem like they present the new syntax as "easier to understand and maybe more performant".
On the other hand, if we were to presume an equivalent
Pointdefinition likethe user code could look like
Sure, that doesn't do the work all on one line and during the destructuring, but the result is the same and much clearer to a reader, given that each part of the code is only doing one thing.
Ok, I'll grant you that in this case we do save one access of
shape.p1andshape.p2compared to what's possible withswitch, which also requires thebreak:Presuming that a
Lineclass was being used, and with perhaps more common JS, those issues can be avoided:On the other hand, if we were to presume that
Pointprovided public accessorsxandy, our code could look like:If the author of
Pointdid require always getting both coordinates at the same time, then it'd probably make more sense to handle the default padding in the body:Actually, having written out those alternatives, this last one is by far the clearest to me, as each part of it is only doing one thing, and it's much clearer that extracting the values actually involves some work.
Why should having this code in the function body be considered a bad thing?