A proposal to introduce Extractor Objects and Extractor Patterns to ECMAScript.
This would augment the syntax for BindingPattern and AssignmentPattern to allow for new destructuring forms, as in the following example:
// binding patterns
const Foo(y) = x; // instance-array destructuring
const Foo{y} = x; // instance-object destructuring
const [Foo(y)] = x; // nesting
const [Foo{y}] = x; // ..
const { z: Foo(y) } = x; // ..
const { z: Foo{y} } = x; // ..
const Foo(Bar(y)) = x; // ..
const X.Foo(y) = x; // qualified names (i.e., a.b.c)
// assignment patterns
Foo(y) = x; // instance-array destructuring
Foo{y} = x; // instance-object destructuring
[Foo(y)] = x; // nesting
[Foo{y}] = x; // ..
({ z: Foo(y) } = x); // ..
({ z: Foo{y} } = x); // ..
Foo(Bar(y)) = x; // ..
X.Foo(y) = x; // qualified names (i.e., a.b.c)
In addition, this would leverage the new global symbol @@matcher
added by the Pattern Matching proposal. When
destructuring using the new form, the @@matcher
method would be called and its result would be destructured instead.
This proposal is heavily influenced by the Pattern Matching proposal as well as forthcoming proposals for enum
. The intent
is to dovetail with these proposals to create a concise syntax when working with ADT (algebraic data type) enums:
// Rust-like enum of algebraic data types:
enum Option of ADT {
Some(value),
None
}
// construction
const x = Option.Some(1);
// destructuring
const Option.Some(y) = x;
y; // 1
// pattern matching
match (x) {
when (Option.Some(y)): console.log(y); // 1
when (Option.None): console.log("none");
}
// Another ADT enum example:
enum Message of ADT {
Quit,
Move{x,y},
Write(message),
ChangeColor(r,g,b),
}
// construction
const msg1 = Message.Move{ x: 10, y: 10 }; // NOTE: possible novel sytax for enum construction
const msg2 = Message.Write("Hello");
const msg3 = Message.ChangeColor(0x00, 0xff, 0xff);
// destructuring
const Message.Move{x,y} = msg1; // x: 10, y: 10
const Message.Write(message) = msg2; // message: "Hello"
const Message.ChangeColor(r, g, b); // r: 0, g: 255, b: 255
// pattern matching
match (msg) {
when (Message.Move{x, y}): /*...*/;
when (Message.Write(message)): /*...*/;
when (Message.ChangeColor(r, g, b)): /*...*/;
when (Message.Quit): /*...*/;
}
However, extractors have additional utility outside of enums as they allow you to apply custom logic during destructuring:
// A custom extractor to re-interpret a value as an Instant
const InstantExtractor = {
[Symbol.matcher](value) {
if (value instanceof Temporal.Instant) return { matched: true, value: [value] };
else if (value instanceof Date) return { matched: true, value: [Temporal.Instant.fromEpochMilliseconds(value.getTime())] };
else if (typeof value === "string") return { matched: true, value: [Temporal.Instant.from(value)] };
// NOTE: `value` here is currently unused, but it could be potentially useful for better error messages:
else return { matched: false, value: new TypeError("Value cannot be converted to an Instant.") };
}
};
class Book {
constructor({
isbn,
title,
// Extract `createdAt` as an Instant
InstantExtractor(createdAt) = Temporal.Now.instant(),
InstantExtractor(modifiedAt) = createdAt
}) {
this.isbn = isbn;
this.title = title;
this.createdAt = createdAt;
this.modifiedAt = modifiedAt;
}
}
new Book({ isbn: "...", title: "...", createdAt: Temporal.from("...") });
new Book({ isbn: "...", title: "...", createdAt: "..." });
new Book({ isbn: "...", title: "...", createdAt: new Date() });
++ QualifiedName[Yield, Await] :
++ IdentifierReference[?Yield, ?Await]
++ QualifiedName[?Yield, ?Await] `.` IdentifierName
BindingPattern[Yield, Await] :
ObjectBindingPattern[?Yield, ?Await]
ArrayBindingPattern[?Yield, ?Await]
++ QualifiedName[?Yield, ?Await] ExtractorBindingPattern[?Yield, ?Await]
++ ExtractorBindingPattern[Yield, Await] :
++ ExtractorObjectBindingPattern[?Yield, ?Await]
++ ExtractorArrayBindingPattern[?Yield, ?Await]
++ ExtractorObjectBindingPattern[Yield, Await] :
++ ObjectBindingPattern[?Yield, ?Await]
++ ExtractorArrayBindingPattern[Yield, Await] :
++ `(` Elision? BindingRestElement[?Yield, ?Await]? `)`
++ `(` BindingElementList[?Yield, ?Await] `)`
++ `(` BindingElementList[?Yield, ?Await] `,` Elision? BindingRestElement[?Yield, ?Await]? `)`
AssignmentPattern[Yield, Await] :
ObjectAssignmentPattern[?Yield, ?Await]
ArrayAssignmentPattern[?Yield, ?Await]
++ QualifiedName[?Yield, ?Await] ExtractorAssignmentPattern[?Yield, ?Await]
++ ExtractorAssignmentPattern[Yield, Await] :
++ ExtractorObjectAssignmentPattern[?Yield, ?Await]
++ ExtractorArrayAssignmentPattern[?Yield, ?Await]
++ ExtractorObjectAssignmentPattern[Yield, Await] :
++ ObjectAssignmentPattern[?Yield, ?Await]
++ ExtractorArrayAssignmentPattern[Yield, Await] :
++ `(` Elision? BindingRestElement[?Yield, ?Await]? `)`
++ `(` BindingElementList[?Yield, ?Await] `)`
++ `(` BindingElementList[?Yield, ?Await] `,` Elision? BindingRestElement[?Yield, ?Await]? `)`
++ FunctionCall[Yield, Await] :
++ CallExpression[?Yield, ?Await] Arguments[?Yield, ?Await]
CallExpression[Yield, Await] :
CoverCallExpressionAndAsyncArrowHead[?Yield, ?Await]
SuperCall[?Yield, ?Await]
ImportCall[?Yield, ?Await]
++ FunctionCall[?Yield, ?Await]
-- CallExpression[?Yield, ?Await] Arguments[?Yield, ?Await]
CallExpression[?Yield, ?Await] `[` Expression[+In, ?Yield, ?Await] `]`
CallExpression[?Yield, ?Await] `.` IdentifierName
CallExpression[?Yield, ?Await] TemplateLiteral[?Yield, ?Await, +Tagged]
CallExpression[?Yield, ?Await] `.` PrivateIdentifier
The examples in this section use a desugaring to explain the underlying semantics, given the following helper:
function %InvokeCustomMatcher%(val, matchable) {
// see https://tc39.es/proposal-pattern-matching/#sec-custom-matcher
}
function %InvokeCustomMatcherOrThrow%(val, matchable) {
const result = %InvokeCustomMatcher%(val, matchable);
if (result === ~not-matched~) {
throw new TypeError();
}
return result;
}
The statement
const Foo(y) = x;
is approximately the same as the following transposed representation
const [y] = %InvokeCustomMatcherOrThrow%(Foo, x);
The statement
const Foo{y} = x;
is approximately the same as the following transposed representation
const {y} = %InvokeCustomMatcherOrThrow%(Foo, x);
The statement
const Foo(Bar(y)) = x;
is approximately the same as the following transposed representation
const [_a] = %InvokeCustomMatcherOrThrow%(Foo, x);
const [y] = %InvokeCustomMatcherOrThrow%(Bar, _a);
Given the following definition
const MapExtractor = {
[Symbol.matcher](map) {
const obj = {};
for (const [key, value] of map) {
obj[typeof key === "symbol" ? key : `${key}`] = value;
}
return { matched: true, value: obj };
}
};
const obj = {
map: new Map([["a", 1], ["b", 2]])
};
The statement
const { map: MapExtractor{ a, b } } = obj;
is approximately the same as the following transposed representation
const { map: _temp } = obj;
const { a, b } = %InvokeCustomMatcherOrThrow%(MapExtractor, _temp);
The syntax-directed operation BindingInitialization takes arguments value and environment.
++ BindingPattern : QualifiedName ExtractorBindingPattern
- Let ref be the result of evaluating QualifiedName.
- Let extractor be ? GetValue(ref).
- Let obj be ? InvokeCustomMatcherOrThrow(extractor, value).
- Return the result of performing BindingInitialization of ExtractorBindingPattern with arguments obj and environment.
++ ExtractorBindingPattern : ExtractorArrayBindingPattern
- Let iteratorRecord be ? GetIterator(value).
- Let result be IteratorBindingInitialization of ExtractorBindingPattern with arguments iteratorRecord and environment.
- If iteratorRecord.[[Done]] is false, return ? IteratorClose(iteratorRecord, result).
- Return result.
- Let result be ? InvokeCustomMatcher(val, matchable).
- If result is
not-matched, throw a TypeError exception. - Return result.
The syntax-directed operation IteratorBindingInitialization takes arguments iteratorRecord and environment.
ArrayBindingPattern : `[` `]`
++ ExtractorBindingPattern : `(` `)`
- Return NormalCompletion(empty).
ArrayBindingPattern : `[` Elision `]`
++ ExtractorBindingPattern : `(` Elision `)`
- Return the result of performing IteratorDestructuringAssignmentEvaluation of Elision with iteratorRecord as the argument.
ArrayBindingPattern : `[` Elision? BindingRestElement `]`
++ ExtractorBindingPattern : `(` Elision? BindingRestElement `)`
- If Elision is present, then
- Perform ? IteratorDestructuringAssignmentEvaluation of Elision with iteratorRecord as the argument.
- Return the result of performing IteratorBindingInitialization for BindingRestElement with iteratorRecord and environment as arguments.
ArrayBindingPattern : `[` BindingElementList `,` Elision `]`
++ ExtractorBindingPattern : `(` BindingElementList `,` Elision `)`
- Perform ? IteratorBindingInitialization for BindingElementList with iteratorRecord and environment as arguments.
- Return the result of performing IteratorDestructuringAssignmentEvaluation of Elision with iteratorRecord as the argument.
ArrayBindingPattern : `[` BindingElementList `,` Elision? BindingRestElement `]`
++ ExtractorBindingPattern : `(` BindingElementList `,` Elision? BindingRestElement `)`
- Perform ? IteratorBindingInitialization for BindingElementList with iteratorRecord and environment as arguments.
- If Elision is present, then
- Perform ? IteratorDestructuringAssignmentEvaluation of Elision with iteratorRecord as the argument.
- Return the result of performing IteratorBindingInitialization for BindingRestElement with iteratorRecord and environment as arguments.
AssignmentExpression : LeftHandSideExpression = AssignmentExpression
If LeftHandSideExpression is an ObjectLiteral or, an ArrayLiteral, or a FunctionCall the following Early Error rules are applied:
- It is a Syntax Error if LeftHandSideExpression is not covering an AssignmentPattern.
- All Early Error rules for AssignmentPattern and its derived productions also apply to the AssignmentPattern that is covered by LeftHandSideExpression.
If LeftHandSideExpression is neither an ObjectLiteral, nor an ArrayLiteral, nor a FunctionCall, the following Early Error rule is applied:
- It is a Syntax Error if AssignmentTargetType of LeftHandSideExpression is not
simple
.
AssignmentExpression :
LeftHandSideExpression AssignmentOperator AssignmentExpression
LeftHandSideExpression `&&=` AssignmentExpression
LeftHandSideExpression `||=` AssignmentExpression
LeftHandSideExpression `??=` AssignmentExpression
- It is a Syntax Error if AssignmentTargetType of LeftHandSideExpression is not
simple
.
AssignmentExpression : LeftHandSideExpression `=` AssignmentExpression
- If LeftHandSideExpression is neither an ObjectLiteral, nor an ArrayLiteral, nor a FunctionCall, then
- Let lref be the result of evaluating LeftHandSideExpression.
- ReturnIfAbrupt(lref).
- If IsAnonymousFunctionDefinition(AssignmentExpression) and IsIdentifierRef of LeftHandSideExpression are both true, then
- Let rval be NamedEvaluation of AssignmentExpression with argument lref.[[ReferencedName]].
- Else,
- Let rref be the result of evaluating AssignmentExpression.
- Let rval be ? GetValue(rref).
- Perform ? PutValue(lref, rval).
- Return rval.
- Let assignmentPattern be the AssignmentPattern that is covered by LeftHandSideExpression.
- Let rref be the result of evaluating AssignmentExpression.
- Let rval be ? GetValue(rref).
- Perform ? DestructuringAssignmentEvaluation of assignmentPattern using rval as the argument.
- Return rval.
The syntax-directed operation DestructuringAssignmentEvaluation takes argument value. It is defined piecewise over the following productions:
++ AssignmentPattern : QualifiedName ExtractorAssignmentPattern
- Let ref be the result of evaluating QualifiedName.
- Let extractor be ? GetValue(ref).
- Let obj be ? InvokeCustomMatcherOrThrow(extractor, value).
- Return the result of performing DestructuringAssignmentEvaluation of ExtractorAssignmentPattern with argument obj.
ArrayAssignmentPattern : `[` `]`
++ ExtractorArrayAssignmentPattern : `(` `)`
- Let iteratorRecord be ? GetIterator(value).
- Return ? IteratorClose(iteratorRecord, NormalCompletion(empty)).
ArrayAssignmentPattern : `[` Elision `]`
++ ExtractorArrayAssignmentPattern : `(` Elision `)`
- Let iteratorRecord be ? GetIterator(value).
- Let result be IteratorDestructuringAssignmentEvaluation of Elision with argument iteratorRecord.
- If iteratorRecord.[[Done]] is false, return ? IteratorClose(iteratorRecord, result).
- Return result.
ArrayAssignmentPattern : `[` Elision? AssignmentRestElement `]`
++ ExtractorArrayAssignmentPattern : `(` Elision? AssignmentRestElement `)`
- Let iteratorRecord be ? GetIterator(value).
- If Elision is present, then
- Let status be IteratorDestructuringAssignmentEvaluation of Elision with argument iteratorRecord.
- If status is an abrupt completion, then
- Assert: iteratorRecord.[[Done]] is true.
- Return Completion(status).
- Let result be IteratorDestructuringAssignmentEvaluation of AssignmentRestElement with argument iteratorRecord.
- If iteratorRecord.[[Done]] is false, return ? IteratorClose(iteratorRecord, result).
- Return result.
ArrayAssignmentPattern : `[` AssignmentElementList `]`
++ ExtractorArrayAssignmentPattern : `(` AssignmentElementList `)`
- Let iteratorRecord be ? GetIterator(value).
- Let result be IteratorDestructuringAssignmentEvaluation of AssignmentElementList with argument iteratorRecord.
- If iteratorRecord.[[Done]] is false, return ? IteratorClose(iteratorRecord, result).
- Return result.
ArrayAssignmentPattern : `[` AssignmentElementList `,` Elision? AssignmentRestElement? `]`
++ ExtractorArrayAssignmentPattern : `(` AssignmentElementList `,` Elision? AssignmentRestElement? `)`
- Let iteratorRecord be ? GetIterator(value).
- Let status be IteratorDestructuringAssignmentEvaluation of AssignmentElementList with argument iteratorRecord.
- If status is an abrupt completion, then
- If iteratorRecord.[[Done]] is false, return ? IteratorClose(iteratorRecord, status).
- Return Completion(status).
- If Elision is present, then
- Set status to the result of performing IteratorDestructuringAssignmentEvaluation of Elision with iteratorRecord as the argument.
- If status is an abrupt completion, then
- Assert: iteratorRecord.[[Done]] is true.
- Return Completion(status).
- If AssignmentRestElement is present, then
- Set status to the result of performing IteratorDestructuringAssignmentEvaluation of AssignmentRestElement with iteratorRecord as the argument.
- If iteratorRecord.[[Done]] is false, return ? IteratorClose(iteratorRecord, status).
- Return Completion(status).