This proposal defines new syntax for destructuring binding with tagged template literals.
Not submitted.
This proposal introduces the template binding literal, a destructuring binding pattern which mirrors the template literal expression. In addition to interpolated values (via ${value}
), template binding literals can also interpolate bindings, using the syntax @{binding}
. These bindings can themselves be destructured.
While template binding literals can be used anywhere a destructuring binding is used (e.g. in variable declarations or function parameters), they will be most useful for pattern matching.
const text = "The current temperature is 31 F";
const `The current temperature is @{degrees} @{unit}` = text;
// degrees = "31"
// unit = "F"
roughly equivalent to:
const text = "The current temperature is 31 F";
const [_, degrees, unit] = text.match(
/^The current temperature is (.*?) (.*?)$/
);
const text = "1999-12-31";
const re`(@{year}[0-9]{4})-(@{month}[0-9]{2})-(@{day}[0-9]{2})` = text;
// year = "1999"
// month = "12"
// day = "31"
const barKey = Symbol('bar');
const map = new Map([
["foo", 1],
[barKey, 2],
["baz", 3],
["quux", 4]
]);
const m`{ foo => @{foo}, ${barKey} => @{bar}, ...@{rest} }` = map;
// foo = 1
// bar = 2
// rest = new Map([["baz", 3], ["quux", 4]])
case (action) {
// matches `{ type: "set-visibility-filter", payload: visFilter }`
when t`set-visibility-filter @{visFilter}` -> ({...state, visFilter}),
// matches `{ type: "toggle-sort-direction" }` but binds no values
when t`toggle-sort-direction` -> ({ ...state, sortDirection: !state.sortDirection }),
// matches everything else
when _ -> state
}
Tagged template binding literals desugar as function calls, similarly to their expression counterparts. If the match succeeds, the function should return an array of values which correspond to the values being bound. If the match fails, the function should return null. This is somewhat similar to the format used by RegExp match/exec results, but with only the captured substrings.
const m`{ "foo" => @{foo = 0}, ${barKey} => @{{ x, y }} }` = myMap;
desugars to:
const [foo = 0, { x, y }] = m(
// first argument: value being matched
myMap,
// second argument: template string array
['{ "foo" => ', ", ", " => ", " }"],
// rest arguments: bindings & interpolated values
// bindings are represented with well-known symbol `Symbol.binding`
Symbol.binding,
barKey,
Symbol.binding
);
This function implements the same behavior as an untagged template binding literal:
function matchString(subject, stringParts, ...interpolations) {
let regExpString = "^";
for (const str of stringParts) {
regExpString += escapeRegexp(str);
if (interpolations.length) {
const interp = interpolations.shift();
if (interp === Symbol.binding) {
regExpString += "(.*?)";
} else {
regExpString += escapeRegexp(String(interp));
}
}
}
regExpString += "$";
const result = new RegExp(regExpString).exec(subject);
if (!result) return null;
return result.slice(1);
}
At this point, we must acknowledge that it is rather odd to propose pattern matching via tagged template literals and not pattern matching with function calls. Pattern matching with type constructors is quite common in typed functional languages, and some of them also allow pattern matching with arbitrary functions that take additional parameters, e.g. F#'s Active Patterns.
However, this can result in a somewhat counterintuitive parsing to distinguish between arguments to the pattern function and the pattern being extracted from it. Consider the code:
let datePattern = "(\d{1,4})-(\d{1,2})-(\d{1,2})"
match str with
| ParseRegex datePattern [Integer y; Integer m; Integer d]
-> new System.DateTime(y, m, d)
The equivalent form in JS would be something like:
const datePattern = /(\d{1,4})-(\d{1,2})-(\d{1,2})/;
case (str) {
when ParseRegex(datePattern, [y, m, d]) -> new Date(y, m, d),
}
In both of these examples, the pattern ParseRegex
and its first argument datePattern
are understood to be expressions, while its last "argument" (the array) is actually the pattern applied to its return value. The potential confusion is mitigated in F# -- the absence of parentheses makes this read a little better, and the type system can prevent you from using the wrong argument order. JavaScript has no affordances here, and this syntax would cause significant confusion.
On the other hand, tagged template binding literals explictly distinguish between interpolated bindings and values, and the relative obscurity of (regular) tagged template literals means that there will be comparatively fewer expectations to violate.
ES6 tagged template literals were heavily influenced by quasi-literals in E, and were even initially called "quasi-literals". Quasis in E can also create bindings (with @{binding}
syntax), though they are used on the right-hand side of an =~
expression, not the left-hand side of an assignment:
def line := "By the rude bridge that arched the flood"
if (line =~ `@word1 @{word2} rude @remainder`) {
println(`text matches, word1 = $word1`)
}
from E in a Walnut
Was there ever a point where the template tag literals proposal also could create bindings, as they do in E?
Very briefly. There was a JS AST proposal at the time and some trick that passed ASTs of interpolations to a tag function that returned an AST and then there was some trick with eval that let this all be syntactic sugar. It was not well received so never drafted.
My sense of the room was that there was a preference for a good string composition proposal with room for more complex use cases over a bad macros proposal, which in retrospect was a fair characterization.
Mike Samuel, author of the quasis/tagged template ES6 proposal
When named capture groups are used with a literal regexp on the left-hand side of an expression and the
=~
operator, the captured text is also assigned to local variables with corresponding names.
/\$(?<dollars>\d+)\.(?<cents>\d+)/ =~ "$3.67" #=> 0
dollars #=> "3"
Neat example extrapolations, especially integrating it with optional regex, I love that!
I'm not sure if this fits as cleanly into the 'destructuring' model but was thinking that this could easily take an optional parser callback, too? Let's say you're working in a text-based game and you've got regular lines that look like this:
The player has 30 health and 25 gold. The room contains exits to the north and west.
Does that make sense? May be a bit far afield but I feel like there'd be some pretty happy code that would suddenly become pretty easy to write?