Closed codehag closed 6 months ago
Custom matchers could never be added later if they’re not added in the first version, including the ability to modify the pass-through value, which is necessary for RegExps.
thats not true.
I’d love to hear more - once the ability to pass an arbitrary expression is present, we wouldn’t be able to add custom matchers to builtins, because code on the web could depend on ${Map}
eg doing an === Map comparison, rather than “is this an instance of a Map”
I'm gonna paste my reply on Matrix.
Assignment, test, aliasing
I never think of this problem before because I'm very familiar with the status quo proposal, but once Yulia pointed this out, I agree this is a serious problem we need to reconsider.
Even with today's JavaScript, I am sometimes confused in the deconstruction alias syntax { a: b }
which is the binding I can use? If we add one more overload to the { ... }
it indeed will make things much harder to read.
let val when Option.isSome
At the first glance, this makes me think of @rbuckton 's unapply proposal. I'm much in favor of that form in the past, let Option(val) = expr
. But after reading the whole document, I'm worried about if unapply syntax could cover all the abilities in Yulia's document.
Here is a feedback from a normal JavaScript developer:
I don't know too much about those concern (test & assignment, ...), but as a normal user, except isOk and maybeRetry.bind(this). I agree Yulia's version is much more readable.
This is really cool! I think challenging ourselves to reuse existing structures is an extremely worthwhile reframing of things.
In general, the layered proposal idea sounds like a fantastic antidote toward biting off too much unnecessarily. (We should definitely bikeshed a name other than "epic", as that sounds like JIRA and makes me want to quit tech forever 😄 but I think something like "layered proposal" would be descriptive.)
A few specific comments:
I adore the let ... when
idea. when ... with
made me feel like "oh no, there's a with
now? do we really need this too?", but this reveals that my apprehension was not about functionality but rather presentation—we were just racking up too many brand-new constructs at once.
I agree with you in having no specific desire to "kill switch
"—expanding it would seem okay to me, if we had a plan for how it would interact with case
. Otherwise, the key benefit to match
seems to be that it would be an expression (but then there's also do { switch {} }
).
Your regex example addresses a complaint I had! The named capture group vs. match result situation feels totally magical in the current README, but making { groups: ... }
explicit resolves that.
As a relatively minor point, I would want to disallow = when ...
though. As I'm seeing it, it shouldn't just be a standalone expression.
Thanks for doing such a thorough analysis. I hope your wrist heals quickly!
(FWIW, I also don't think you'd need to step down if it's purely due to lack of recent participation; I've been even more distant myself these past months, and it'd be a loss to not have your voice in this context.)
I have read through the proposal and I'll admit I need some time to work through the design changes and put together all of my impressions and concerns, though I'd like to list a few things I have some issues with at first glance.
I'm a bit concerned about the let ... when ...
syntax based on my investigation into extractor objects as an upcoming proposal. In Scala, an extractor is part of the pattern matching mechanism, and a failure to match a pattern when initializing a variable would result in an exception, not a default:
// if customer is a match, populates 'name'
// if customer is not a match, throws 'scala.MatchError'
val CustomerID(name) = customer;
For the extractors proposal, I am considering the same approach. For a variable declaration, a failed match would result in an error since there is no escape hatch for a refutable match. While for match
, you are able to declare alternate match legs or a default
for an irrefutable match.
For the extractor objects proposal, this example would instead be something like:
const IsOk = { [Symbol.matcher]: response => response.status === 200 };
// declaration
let IsOk{ body } = response; // if not ok, throws an error
// match
match (response) {
IsOk{ body }: ...; // if not ok, try next leg
...
}
I'm hoping that extractors will remove some of the need for with
, and will better dovetail with ADT Enums and destructuring, but I don't see that being the case with let...when...
.
I'm also not sure what let ... when ...
gives you over let ... = ... ? ... : ...
in your example:
let { body } when isOk(response);
...
let { body } = isOk(response) ? response : {};
The second example already gives you more control over the non-matching case. It's also not clear how let ... when ...
would choose an appropriate default value. If you write let [x, y] when f(obj)
will it change the default to []
instead of {}
, or will it throw because the default isn't iterable?
What about cases like this:
const x = Math.floor(Math.random() * 1000);
const y when isEven(x);
What is the expected value of y
when x
is odd?
Please, take whatever time you need to respond.
Custom matchers could never be added later if they’re not added in the first version
Yulia does not mean we make an MVP version (like class), it writes we should design them as many different small parts (and ship them at once so it won't be another class
design) that can work well together and can work on their own.
I have the same feeling about the Pipeline proposal, which can be split into Partial Function Application (f(?, 2)
returns a higher order function) and F# style pipeline. In this way, Partial Function Applications can be useful on their own, and the F# style pipeline can get rid of the topic operator.
I see the same idea in Yulia's design so I like it.
We should definitely bikeshed a name other than "epic", as that sounds like JIRA and makes me want to quit tech forever 😄
LOL I agree
but then there's also
do { switch {} }
Oh No, please don't. See my reply at https://github.com/tc39/proposal-do-expressions/issues/75
relation to
unapply
I noticed that there is something that @rbuckton's unapply cannot do but Yulia's version can.
// unapply
if (let Some(val) = expr) {}
// when
if (let val = expr when isSome) {}
This is both two designs can do, but the following is not
// unapply
if (how to do this?) {}
if (let url = response when { status: 301 } {}
// ^ pattern matching here!
Some answers to questions, this time with a bit more detail:
Custom matchers could never be added later if they’re not added in the first version, including the ability to modify the pass-through value, which is necessary for RegExps.
and
I’d love to hear more - once the ability to pass an arbitrary expression is present, we wouldn’t be able to add custom matchers to builtins, because code on the web could depend on ${Map} eg doing an === Map comparison, rather than “is this an instance of a Map”
Jordan and I discussed this in a call -- Custom matchers can be a dependency on which pattern matching is built. This would mean that we do base matchers and custom matchers before we introduce pattern matching syntax. This was my suggestion, and my answer to how custom matchers can be handled separately. In addition, an alternative splitting is one @ljharb suggested: we can split by introducing ${Builtin} at a later point in time. I am not opinionated in how this is done.
To @rbuckton
I'm also not sure what let ... when ... gives you over let ... = ... ? ... : ...
I should have been clearer here: this isn't a perfect analogy. The only thing set here is if let ... when ...
is pattern matched, then it is bound. Otherwise -- we have options in what can happen. My proposal was to be unobstructive and "pass through" an undefined value on failure. Throwing an error is another option. None of this is in stone, these are ideas.
What about cases like this:
const x = Math.floor(Math.random() * 1000); const y when isEven(x);
What is the expected value of y when x is odd?
in my original thinking: undefined. But this is open to discussion. The goal was to get us thinking of the match clause as a base of this proposal, rather than patterns or the match statement itself. Using the match clause as the base allows us to first work with base matchers, then custom matchers, then syntax.
We should definitely bikeshed a name other than "epic", as that sounds like JIRA and makes me want to quit tech forever 😄
happy with "layered proposal", thats a good suggestion
Yulia does not mean we make an MVP version (like class), it writes we should design them as many different small parts (and ship them at once so it won't be another class design) that can work well together and can work on their own.
This is largely correct: the idea is to fix our tendency to do versioned proposals (like classes), and instead have a clear story for a more complex proposal like this one, that allows room for each piece to breath and be considered carefully. It brings necessary scrutiny to the constituent parts of the proposal. For me, this exercise was educational. It is discouraging to have it always said that this is impossible, and is likely why i didn't attempt it before. But this reaction is understandable -- we've been burnt by MVPs before. Anyway, we are trying to help eachother here, my goal is not to throw you all off track or ask that you change everything. Due to a long day at work i mixed two separate concepts -- my identification of a potential syntax issue, and the question of "how might we split this while preserving the key parts".
As for shipping: sometimes we conflate shipping with stage 3. I would say stage 3 is fine, even encouraged as it will allow us to write tests and start implementing (potentially informing the design). I would also say -- shipping the proposal as a whole or piece meal is at the discretion of the champions, and they can communicate that. we have precedent for this.
I've updated the title to hopefully be more about the reframe, rather than "simplification" -- as simplification is really about decoupling, not necessarily feature removal.
@codehag and I had a productive call last night, and I've removed pattern matching from this week's agenda now that I more fully understand both her and the SpiderMonkey team's feedback.
The tl;dr as I understood it:
In early August, I'll reach out to champions (and @codehag) to schedule a meeting to discuss the proposal. With any luck, we'll be able to present a compelling update in September that will preemptively address this feedback, and consider if we're again ready for stage 2 advancement after processing feedback from that plenary.
I'm happy about the decision to take time to discuss this proposal further.
About custom matchers coming later: I really want custom matchers to happen, but I could definitely imagine them coming post-MVP. All we'd need to do is say, if you use a primitive as a matcher, it checks by ===
(or similar), and if you use an object, an exception is unconditionally thrown. If someone wants to compare by ===
, they can do so in an if
guard. This would significantly reduce the forward compatibility risk of adding custom matchers later.
Dropping custom matchers from the initial iteration would be a significant reduction in scope, for sure. Sure it would make pattern matching less powerful, but it would still be extremely useful.
Notably, doing that would give @rbuckton more time to progress the Extractors proposal, which could potentially then serve as a base for custom matcher syntax (an approach that has precedent in Scala and some other languages).
Thanks so much for pulling this example together, @codehag.
In terms of the general approach, I especially admire the way that by looking at the proposal in terms of layers, opportunities arise to decompose changes into elements that are larger than the proposal but in keeping with the grain of the language. The way the base case and implicit values expand into catch guards, for instance.
Regarding this specific proposal, while it is certainly not required that layers be implemented slowly, I would like to reiterate my comment from the previous plenary that doing so allows for our understandings of usage to deepen as functionality is used. In nearly every plenary, I hear people talk about "mistakes" that we are now stuck with, and it seems like letting functionality ripen will help avoid some mistakes of over- or mistaken design.
Even more specifically, I find the Fixing switch section the most compelling and really the core of the proposal. While other, more complex uses of matching can be conceived, this is the main existing problem; solutions built out of this will likely be the most robust, it seems to me.
I do have a bit of concern on waiting for the extractors proposal. Suddenly making assignment throw instead of resulting in undefined, while currently fashionable, may not be desirable long-term, especially in the sense of creating mixed metaphors and countering reasonably stable expectations.
Anyways, thanks again! Can't wait to see where this all goes.
I wrote up https://gist.github.com/rbuckton/e49581c9031a73edd0fce7a260748994 to show how the Extractors proposal I have been working on could potentially provide the benefits from @codehag's proposal by providing counter-examples. I quite like the idea of breaking down pattern matching into pieces that are composable, though I differ from pattern-matching-epic in a number of syntactic choices.
Thanks @rbuckton -- this was an interesting read. Importantly, I don't care too much about the exact syntax, so I am very open to counter proposals. A few thoughts:
// extractors
let isOk{ body } = response;
const isOk{ body } = response;
var isOk{ body } = response;
this looks pretty interesting. I am also very interested in how this is expressed in the for loops:
// continue if isOk is not true
for (let isOk{ body } of responses) {
handle(body);
}
I very much like the consistency.
for the match statement:
match (command) {
when (isGo(, dir)): go(dir);
when (isTake(, item)): take(item);
default: lookAround();
}
I would have expected (but understand why this is impossible):
match (command) {
when (isGo[, dir]): go(dir);
when (isTake[, item]): take(item);
default: lookAround();
}
Something I like about the strategy overall: This feels like it falls in line with tagged template functions, ie isOk\
some string``. What if this was ubiquitous in the language? that is an interesting question.
A draw back: It is unfortunate it cannot be applied to arrays. However in the current form I think this doesn't work. It is too close to function invocation. At least, in my opinion right now -- maybe this could be like function arguments. But then this must apply consistently.
This is where it breaks down for me. It results in too much inconsistency. I am open to being shown that it is consistent though. Otherwise, my preference is for the current proposal with meaningful left and right hand sides.
Still, it is fun to play with.
match (result) {
when (Option.Some{ value }): console.log(value);
when (Option.None): console.log("none");
}
Not bad at all in my opinion. I was thinking along these lines when I wrote this up, but it felt too radical:
match (result) {
when Option.Some { value }: console.log(value);
when Option.None: console.log("none");
}
That said there may be a happy middle, which may work for you as well and was mentioned by jordan. We are close to the existing with
syntax at this point. So why not:
match (result) {
when Option.Some as { value }: console.log(value);
when Option.None: console.log("none");
}
But this needs more thought. It still moves the assignment to the right, which is hard to find quickly. But this still aligns with my push to reuse existing language constructs.
while (responses.pop() is { status: 200, body: let body }) {
handle(body);
}
Thats pretty neat. I hadn't thought about the let in that position. It does sort of drop my goal of having the right hand side always be a test for patterns though.
regarding is
and other positions for patterns -- I didn't have much time to think about it at the time, and I don't have strongly held opinions. Open to discussion. I would eagerly discard it to be honest, it was mostly to enable the pattern matching to be used elsewhere.
This proposed separation also does not work well with pattern matching when two disjoint tests could produce the same binding, and therefore leverage the same match leg:
match (obj) { when({ givenName } or { name: { given: givenName }}): console.log(`Hello, ${givenName}`); ... }
Point taken, though the "does not work well " is intentional. Thus, the counter point: This is still easier to read and that should be our goal:
match (obj) {
let when ({ givenName}) : console.log(`Hello, ${givenName}`);
let when ({name: { given }}): console.log(`Hello, ${given}`);
}
This isn't all that longer than the current proposal, and we remove the confusion introduced by the aliasing. I think this is significantly easier to read. The argument "oh but the body of the match is really big" doesn't work here -- this is why functions exist. We have a fully featured language. The ability to write things in one line is not the benefit it is being made out to be here.
As an aside, by the way, this is why, in my opinion, we should not be letting go of fall through. For instance:
match (obj) {
let when ({ givenName}) :
let { name: { given: givenName }} when ({name: { given }}): console.log(`Hello, ${givenName}`);
}
So we could make it possible, but the allergy to switch has made it impossible. That said, my preference is for the example above rather than this one.
If this will throw, why allow it in the first place?
good point, can be a syntax error possibly
I am not clear here on what you mean by "the shadowing issue", but I've already addressed this specific example above.
Consider:
const url = "http://xyz"
match (x) {
when ({ status: 500, destination: url }): handle(x, url)
}
If you were not familiar with this proposal, what is going on here? given that aliasing is a problem syntax for developers, and even committee members, what is the intention? Consider, instead:
const url = "http://xyz"
match (x) {
let { destination: url } when ({ status: 500, destination: url }): handle(x, url)
}
here the intention is clear(er). Or as much as can be so given the mistake we made with destructuring and aliasing. This also opens up the right hand side of the when statement to matchers. This makes it consistent. The right hand side of a :
in a when always tests.
let-when, on the other hand, only allows destructuring without :
.
Let me know if there were other segments of your document you want to have more attention on. I was just picking out the bits that I found interesting.
@codehag: Thank you for your thorough review.
I would have expected (but understand why this is impossible):
match (command) { when (isGo[, dir]): go(dir); when (isTake[, item]): take(item); default: lookAround(); }
While []
might seem more consistent when comparing to something like tuples or array destructuring, the Extractor concept is partially modeled on symmetry with construction/application in Scala, with an Extractor representing the "unapplication":
import scala.util.Random
object CustomerID {
def apply(name: String) = s"$name--${Random.nextLong}"
def unapply(customerID: String): Option[String] = {
val stringArray: Array[String] = customerID.split("--")
if (stringArray.tail.nonEmpty) Some(stringArray.head) else None
}
}
val customerID = CustomerID("Nico") // application
val CustomerID(name) = customerID // unapplication
println(name) // prints: Nico
Since an argument list in JavaScript is very Array-like (especially in strict mode), using ()
for both construction and extraction seems fairly consistent to me.
Something I like about the strategy overall: This feels like it falls in line with tagged template functions, ie
isOk`some string`
. What if this was ubiquitous in the language? that is an interesting question.
I have been considering this as well in relation to ADT-enums, and as a general purpose construction mechanism. For example, it might be useful to be able to perform initial assignments for classes, structs, Map
, etc.:
// class construction
const point = new Point{ x: 10, y: 20 };
const map1 = new Map{ a: 1, b: 2 };
const map2 = new Map(map1) { c: 3, d: 4 };
// enum/struct/value allocation
const message = Message.Resize{ height: 100, width: 200 };
// css-in-js
const styles = CSS{
border: "solid black 1px",
background: "green",
};
How that would be accomplished is still up in the air. For tagged templates, we invoke the function with a specially crafted argument list. We could either do the same here, or introduce a symbol-named mechanism to reduce overload friction, i.e.:
// used with `new F{ ... }` or `new F(){ ... }`
Map[Symbol.propertySetConstruct] = function (propertySet, ...args) {
const map = new Map(...args);
for (const [key, value] of Object.entries(propertySet)) {
map.set(key, value);
}
return map;
}
const CSS = {
// used with `F{ ... }` or `F(){ ... }`
[Symbol.propertySetCall](propertySet) {
const styles = ...;
...;
return styles;
}
};
That said there may be a happy middle, which may work for you as well and was mentioned by jordan. We are close to the existing
with
syntax at this point. So why not:match (result) { when Option.Some as { value }: console.log(value); when Option.None: console.log("none"); }
Introducing an as
keyword here would break the intended symmetry with declaration, construction, and destructuring that I am hoping to achieve in tandem with an updated enum
proposal that supports ADT-style enums:
enum Option of ADT {
Some{ value }, // declaration
None
}
// construction
const opt = Option.Some{ value };
// destructuring
const Option.Some{ value } = opt;
match (opt) {
when(Option.Some{ value }): ...; // pattern matching with `match`
}
if (opt is Option.Some{ value: 1 }) ...; // pattern matching with `is`
I'm also concerned about potential collision or confusion with the as
typecast syntax present in TypeScript and the Stage 1 Type Annotations proposal, especially in conjunction with something like an is
infix operator. Something like x is Foo as { y }
becomes increasingly confusing at a glance.
Introducing as
would also complicate nested pattern matching:
// with 'as'
match (x) {
when (Option.Some as { value: Message.Move as { x, y } }): ...;
}
// without 'as'
match (x) {
when (Option.Some{ value: Message.Move{ x, y } }): ...;
}
The as
just seems to add more visual noise that I don't believe is warranted.
This proposed separation also does not work well with pattern matching when two disjoint tests could produce the same binding, and therefore leverage the same match leg:
match (obj) { when({ givenName } or { name: { given: givenName }}): console.log(`Hello, ${givenName}`); ... }
Point taken, though the "does not work well " is intentional. Thus, the counter point: This is still easier to read and that should be our goal:
match (obj) { let when ({ givenName}) : console.log(`Hello, ${givenName}`); let when ({name: { given }}): console.log(`Hello, ${given}`); }
This isn't all that longer than the current proposal, and we remove the confusion introduced by the aliasing. I think this is significantly easier to read. The argument "oh but the body of the match is really big" doesn't work here -- this is why functions exist. We have a fully featured language. The ability to write things in one line is not the benefit it is being made out to be here.
As an aside, by the way, this is why, in my opinion, we should not be letting go of fall through. For instance:
match (obj) { let when ({ givenName}) : let { name: { given: givenName }} when ({name: { given }}): console.log(`Hello, ${givenName}`); }
So we could make it possible, but the allergy to switch has made it impossible. That said, my preference is for the example above rather than this one.
A number of the pattern matching champions have expressed the opinion that implicit fall-through is bad and should be avoided at all costs. I don't personally share this opinion, but even with fall-through I would argue that supporting logical patterns with duplicate bindings is consistent with the recent change to allow duplicate named capture groups in regular expression patterns. It also is potentially more efficient as it avoids re-evaluating custom matchers for each match leg.
I am not clear here on what you mean by "the shadowing issue", but I've already addressed this specific example above.
Consider:
const url = "http://xyz" match (x) { when ({ status: 500, destination: url }): handle(x, url) }
If you were not familiar with this proposal, what is going on here? given that aliasing is a problem syntax for developers, and even committee members, what is the intention? Consider, instead:
const url = "http://xyz" match (x) { let { destination: url } when ({ status: 500, destination: url }): handle(x, url) }
here the intention is clear(er). Or as much as can be so given the mistake we made with destructuring and aliasing. This also opens up the right hand side of the when statement to matchers. This makes it consistent. The right hand side of a
:
in a when always tests.
I would be more partial to explicit binding declarations via inline let
/const
bindings:
const url = "http://xyz"
match (x) {
when ({ status: 500, destination: let url }): handle(x, url)
}
I still find the let ... when ...
syntax in match
to be unnecessarily repetitive with respect to matching and then declaring bindings and would more than likely fall back to the let when
syntax for almost all of my use cases. And in those cases, if I'm going to have a let
keyword, I'd rather it be used to visually distinguish the actual binding in the pattern rather than be a prefix to the entire clause.
let-when, on the other hand, only allows destructuring without
:
.
This restriction concerns me as it explicitly puts let when
in a position where it can't handle shadowing at all. This pushes me even more towards a preference for explicit inline bindings.
Let me know if there were other segments of your document you want to have more attention on. I was just picking out the bits that I found interesting.
My goal for Extractors is a capability that covers more than just pattern matching, though there is significant overlap. I'm also interested in their use as a general-purpose destructuring mechanism in binding patterns, including function parameters. For example, in my gist for Extractors, I have the following use case:
// A custom extractor to re-interpret a value as an Instant
const InstantExtractor = {
[Symbol.matcher](value) {
if (value instanceof Temporal.Instant) {
// if the value is already an instant, return the value as a match.
return { matched: true, value: [value] };
}
else if (value instanceof Date) {
// if the value is a JS Date, convert it and return a match.
return { matched: true, value: [Temporal.Instant.fromEpochMilliseconds(value.getTime())] };
}
else if (typeof value === "string") {
// if the value is a string, parse it and return a match.
return { matched: true, value: [Temporal.Instant.from(value)] };
}
else {
// the value was not a match
return { matched: false };
}
}
};
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.Instant.from("...") }); // ok, already an instant
new Book({ isbn: "...", title: "...", createdAt: new Date() }); // ok, can convert from date
new Book({ isbn: "...", title: "...", createdAt: "..." }); // ok, can convert from string
new Book({ isbn: "...", title: "...", createdAt: {} }); // error, not a valid match
Whatever we end up for pattern matching, I'd like to ensure we are looking far enough ahead to other potential future capabilities for the language so that we avoid major inconsistencies or paint ourselves into a corner.
After taking another look at the Scala example above, it really does make me wish we already had ADT enums and Option
, as that could make for a better result than the { matched, value }
match result object, i.e.:
// given:
enum Option of ADT {
Some(value),
None
}
// custom matcher
const InstantExtractor = {
[Symbol.matcher]: value => match(value) {
when (Temporal.Instant): Option.Some([value]);
when (Date): Option.Some([Temporal.Instant.fromEpochMilliseconds(value.getTime())]);
when (String): Option.Some([Temporal.Instant.from(value)]);
default: Option.None;
}
}
While I like the general idea of decomposing pattern-matching into multiple proposals, I'm worried about a couple of things based on the conversation thus far:
1. The foundational proposal in both of the proposed split-ups seem to be fairly week.
@codehag's version introduces this syntax in it's foundational, layer-1 proposal
const { body } when isOk(response);
I'll assume the when
syntax causes all LHS bindings to be set to undefined
when the RHS returns false. A more complete code example would then be this:
const { body } when isOk(response);
if (body === undefined) {
...
}
which really isn't that different from what's possible today (plus, today's solution is safer, as it'll correctly handle the case when the response is ok but the body is undefined).
if (!isOk(response)) {
...
}
const { body } = response;
In the end, I feel like this syntax shorthand hasn't provided any value to the language, and it won't be able to do so until we start layering on further proposals. The rest of the foundational proposal showed other ways we could extend this root idea by putting it into for loops or what-not, but none of these are that much of an improvement compared to the alternatives that are possible today.
There's also the option to make when
throw when the pattern-matching fails, but it's also trivial in userland to define an isOk()
function that throws when it's not ok, so you're still not adding much benefit to the language.
@rbuckton's extractor proposal is interesting once the future layers come on, but again, I feel the foundational proposal is lacking.
const isOk{ body } = response;
I assume isOk
is implementing some sort of special protocol to make this work. This sounds like as much work as just doing this:
function isOk(response) {
if (...) {
throw new Error(...);
}
return response;
}
// elsewhere...
const { body } = isOk(response);
I think it's important to split this proposal up in ways that minimizes the amount of "we need to do ABC decision, because DEF and XYZ will need it like that down the line". Of course that sort of thinking will have to happen, as this is an epic we're talking about, so we're still wanting to think how it's constituents proposals interact with each other, but if we've having to do that for every single decision related to a sub-proposal, then we're worse of than where we started. For this reason, I think it's important for every proposal, especially the foundational proposal, to be able to stand on its own legs and carry its own weight, instead of relying on us looking "down the line" to see it's true value.
2. We're introducing new features into the pattern-matching epic that can't be changed or removed, unless we restructure the whole epic.
The const { body } when
syntax is interesting, but what if it's not really for the JavaScript language? Extractors are cool, but, what if they're really not for the JavaScript language? I worry about hinging the entire subdivision of pattern-matching onto concepts like this that haven't yet been extensively discussed. If, in the end, we decide we don't want that sort of syntax, or we want the syntax but in a very different way, we may have to restructure a good portion of the pattern-matching epic.
Alternatives?
Finding a good alternative way to split it up is difficult. I'll try to take a more conservative route which attempts to preserve more of the spirit of the original proposal at the cost of not being able to subdivide it as much.
What if, instead of subdividing pattern-matching into tiny pieces, we instead focused on reducing its size by extracting features from the proposal and moving those into their own "add-on" proposals within the same epic. The core may still be larger, but it doesn't need to be as large as it is today.
And, instead of a layered approach, perhaps a graph approach? You have the core proposal, and a number of add-on proposals that depend on the core, and if needed, proposals can depend on specific add-on proposals (instead of the whole layer).
Here's what this could look like in practice:
The core proposal would still be larger. It needs to be strong enough that it's able to stand on it's own weight (i.e. people would enjoy using it, even if that's the only thing that got released), and have enough in there that we're able to hang everything else off of it, but it should be no larger than that.
Some features the core might include:
${...}
syntax)I think it's important that we consider all of these pieces in the same proposal if we ever hope to get a high-quality syntax that handles each concept well.
The core proposal would also need some sort of match control structure for it to have any value, so we can make it include the match (...) { ... }
stuff from the current proposal. I tried to think of ways to push this off into a separate proposal, but there's really no way to do that without trying to introduce some sort of "lightweight" control structure syntax for the core proposal to use instead that's still powerful enough to show off all of the features from the core proposal (including bindings). There's, perhaps, ways to go that route, but part of my goal was to try and avoid inventing new features and syntaxes as we split this proposal up.
With a powerful core in place, we can move features like the following into add-on proposals:
with
chaining
I recognize that this still leaves the core at a much larger size compared to some of the other epic breakdowns presented, but if we deconstruct the proposal after this manner, it at least feels like the split-apart is less invasive. The other options presented thus far makes it feel like we'll be kicking ourselves back to ground zero, and trying to build ourselves back up using completely new syntax constructs.
(I know in the end that I'm just a community member, who doesn't have to actually worry about how these proposals go through the proposal process, but still, from where I'm standing, I feel like it would be easier to participate in discussions around the proposals if they were able to stand on their own weight, and I would also prefer if we didn't have to start from ground zero when we split it all up)
@rbuckton's extractor proposal is interesting once the future layers come on, but again, I feel the foundational proposal is lacking.
const isOk{ body } = response;
I assume
isOk
is implementing some sort of special protocol to make this work. This sounds like as much work as just doing this: [...]
I need to spend some additional time reading through your comments here, but I would like to point out that the foundational proposal for Extractors is more than a top-level const isOk{ body } = response
. Extractors would allow you to inject custom logic into destructuring at any depth, which isn't feasible today (at least, not in a concise manner):
// nested extractors inside of an array destructuring
const [isOk{ body: body1 }, isOk{ body: body2 }] = await Promise.all([fetchRequest1(), fetchRequest2()]);
// picking apart an Option<Message>
match(result) {
when (Option.Some(Message.Move{ x, y })): console.log(`move: ${x}, ${y}`);
when (Option.Some(Message.Write(text))): console.log(`write: ${text}`);
when (Option.None): console.log(`none`);
}
Extractors would allow you to inject custom logic into destructuring at any depth
Right, I knew that, and forgot that :p. Ok, that does give the extractor base proposal more teeth.
May I ask a couple of clarifying questions with your epic break-apart?
I see that layer 1 brings your extractor proposal in (which, I dug up your rough draft on it to get a better picture, here's the link in case anyone else wants it).
Layer 2 then adds the match syntax, but at this point we don't have an actual concept of patterns. So, in the when (...)
syntax, what sort of stuff is actually legal in the ...
? Is it just extractors to start with? And the future layers have to add more functionality into the match
construct?
To be honest, I've read through layer 3 a handful of times, and I'm struggling to understand what it's trying to additionally introduce (from either break-apart). I'm not sure what's meant by an "implicit value", which seems to be at the core of this layer.
And, layer 4 doesn't seem to add anything in particular - this layer seems to be more for @codehag's original epic separation, which tries to separate assignment from matching, and layer 4 added syntax back in to handle that, which yours didn't have to worry about since you weren't separating the two concepts to begin with.
At what layer do we deal with object and array matchers? (edit: And what about matching against dynamic values, e.g. ${...}
or ^
syntax? And binding patterns? etc) Is that part of layer 2 with the match control structure? Is there any particular reason that, in this version, layer 1 needs to come before layer 2 and not the other way around? I don't see a particularly strong line of dependency between the match control structure and extractors, but I'm also very fuzzy on what goes where as well.
My "epic break-apart" isn't so much a break-apart as it is a diff with comments against @codehag's proposal. I can put together a more cohesive layering focusing specifically on the current pattern matching proposal + extractors, but roughly I'd break it down into the following parts:
const {x} = null
throws, const [x] = {}
throws).Extractor objects are introduced as an extension to destructuring:
Symbol.matcher
method.ObjectExtractorBindingPattern and ObjectExtractorAssignmentPattern syntax is introduced as an extension to object destructuring:
ObjectExtractorBindingPattern :
QualifiedName `{` BindingPropertyList `}`
ObjectExtractorAssignmentPattern:
QualifiedName `{` AssignmentPropertyList `}`
ArrayExtractorBindingPattern and _ArrayExtractorAssignmentPattern` syntax is introduced as an extension to array destructuring:
ArrayExtractorBindingPattern :
QualifiedName `{` BindingElementList `}`
ArrayExtractorAssignmentPattern:
QualifiedName `{` AssigmentElementList `}`
QualifiedName :
IdentifierReference
QualifiedName `.` IdentifierName
Symbol.matcher
is called with the current subject (i.e., the Initializer or parent binding element), and the result is further destructured based on the extractor pattern in use.const Option.Some(value) = obj;
const Message.Move{ x, y } = msg;
const isOk{ body } = response;
const [result, InstantExtractor(start), InstantExtractor(end)] = traceStartEnd(() => someExpensiveOperation());
expr is Pattern
:
true
or false
.0
), bigint, or string literal, or is the identifier undefined
, uses SameValue.true
, false
, or null
, uses SameValue.Infinity
, +Infinity
, or -Infinity
, uses SameValue.+0
or -0
, uses SameValueZeroNaN
, equivalent to isNaN(expr)
.instanceof F
, where F
is a qualified name, equivalent to expr instanceof F
.typeof S
, where S
is a string literal, equivalent to typeof expr === S
.( Pattern )
, returns the result of evaluating is Pattern
against the subject.if (x is undefined) ...;
if (x is typeof "string") ...;
and
, or
, and not
:
not Pattern
, inverts the result of matching Pattern against the subject. For example:
x is not undefined
x is not 0
LeftPattern and RightPattern
, returns true
if both LeftPattern and RightPattern match.LeftPattern or RightPattern
, returns true
if either LeftPattern or rightPattern match.and
, or
, and not
are equivalent to the precedence of infix &&
, ||
, and unary-prefix !
, respectively.if (x is undefined or null) ...;
if (x is typeof "string" or typeof "number") ...;
<
, <=
, >
, and >=
:
if (x is >= 0 and < 10) ...;
match
expression, using layers 2-4.
match (compare(x, y)) {
when (>0): ...;
when (<0): ...;
default: ...;
}
let
/const
bindings in patterns:
if (obj is { x: 10, y: let y }) {
/* y is in scope */
} else {
/* y is in scope, but not initialized */
}
// y is not in scope
when
clause of match
, binding is scoped to when
-leg of the match
expression:
match (obj) {
when ({ x: 10, y: let y }): /* y is in scope */;
default: /* y is not in scope */;
}
Custom matchers introduced, aligns with layers 1-6:
Symbol.matcher
method.true
, subject is the result.matched
is true, the MatchResult's value
is the result.
if (x is Option.Some(let value)) /*use value*/;
match (obj) { when (Message.Move{ x: > 10 and let x, y: > 20 and let y }): / use x and y /; when (Message.Write(let text)): / use text /; default: ...; }
match (x) { when (String and { length: > 0 }): ...; }
There's a bit more to go, and these aren't strictly layers as some things should be merged together.
Thanks for that detailed description @rbuckton, that does help me see your vision clearer.
I do like the idea of having expr is pattern
as well, as that helps break things up into more steps, and I know there's been others in this proposal expressing a desire for something like that. It does, however, feel a bit odd to be able to introduce a declaration into any location where an expression is allowed, I'm sure that would open up a number of odd edge cases to deal with, which might make the overall complexity of figuring out the spec for is
as difficult as the match
construct, at which point it didn't really buy us anything to introduce is
before match
.
Some examples of added complexity:
if (<do declarations here become part of the `if` scope?>) { ... }
for (let i = <Would other variables introduced here persist across all iterations?>; <What happens here?>; <And here?>) { ... }
class MyClass {
x = <what about here?>;
y = <can I access something declared in the previous line from here?>;
}
I know a lot of the above is abusive of this new power, but it's all still complexity that would need to be discussed and ironed out in the same proposal that we're discussing object/array/primitive matchers. I guess another option is to just forbid declarations with is
, and iron that out later or never.
I'm also not entirely sure why extractor objects needs to be the first step in the series of outlined steps. I don't believe future steps directly depend on it being there? (though feel free to clarify on this point). From what I can tell, we could very well start with step 2, the expr is pattern
syntax, bring in patterns for primitives, arrays, and objects, etc. Once that's in place, I believe we could do pretty much all of the other steps you showed in parallel (including extractors). Some of the steps are certainly interdependent, and we could choose one step to place on a layer lower than another in order to prevent them from tripping over each other, but if there's relationship isn't overly strong, perhaps it's ok to let them simply sit on the same layer, and make sure we're aware of how they relate as we work on them. (though, here I'm probably not being my brightest, and am forgetting some important correlations between some of the steps, that would make them difficult to do in parallel).
A couple of responses from my end:
The layering doesnt represent splitting into sub proposals (though admittedly i thought of that initially). Each layer allows us to consider a specific aspect of the proposal in isolation, and that is a significant benefit. This is similar to what the modules compartments proposal is doing, and it looks like other delegates have also noted the problem related to larger, complex, full featured proposals.
- The foundational proposal in both of the proposed split-ups seem to be fairly week.
I don't think so. The foundational proposal in both introduces ubiquitous patterns, similar to Daniel Ehrenberg's Guard proposal. How this is done (if it is done first, or if we leave the capability open for later) is up for discussion. This is really incredibly powerful. What i presented is just a sketch, not intended as a full fledged proposal. It is one way this can be done.
- We're introducing new features into the pattern-matching epic that can't be changed or removed, unless we restructure the whole epic.
I am also a bit wary of deviating too far from the pattern matching proposal as is. A lot of excellent research has gone into it. However, this may give additional argument for delaying decisions on syntax short hand. My priority would be the introduction of patterns first, followed by the ability to author custom matchers.
My goal was to introduce a way to think about this proposal in parts, because in my view -- if we don't, we will force ourselves into situation where we have many inconsistent ways of doing things in the language. My break down is partially in response to the original problem statement:
"There are many ways to match values in the language, but there are no ways to match patterns beyond regular expressions for strings."
This is right on point and an excellent observation. But, if we move forward with the current syntax, which blends de-structuring and pattern matching, we will not be able to achieve it in a way that is not only local to a match statement. Resulting in potential inconsistencies later on.
The layering doesnt represent splitting into sub proposals
Oooh. So, are we still talking about creating layers but having a single proposal repo? And then, in committee meetings, you just work on passing off one layer at a time, instead of trying to discuss the whole giant proposal at once with everyone?
This is right on point and an excellent observation. But, if we move forward with the current syntax, which blends de-structuring and pattern matching, we will not be able to achieve it in a way that is not only local to a match statement. Resulting in potential inconsistencies later on.
This is certainly a valid concern. I don't think this technically means we need to change things now as we're splitting it up to ensure this happens. If we think about it from the "core proposal with a graph of add-ons" idea, another valid path would be to let the core proposal continue to have the current "match" construct and then create a new "add-on" proposal with the "const { body } when isOk(response);"
syntax. This add-on would show flaws that would arise if we try to introduce syntax like this as-is, and will force us to figure out what we'd need to retroactively alter in the core proposal to make this work, and decide if we really want to go this route. Using a more layered model, I'm sure we could push const { body } when isOk(response);
syntax to a higher layer, so it still gets discussed, but we don't have everything building off of it. Or, these don't have to be separate proposals/layers, they could just be discussion threads requesting to change the core, and there the communittee and delegates can hash things out to see how viable and useful the idea is.
I know this makes it harder to split things up finer-grain. I guess my common worry is introducing new features into the proposal primarily to make it easier to split it up. It just seems like a split-up discussion would be much simpler if we only looked at how to split up what we currently have (though, I agree that it would be much harder to do a fine-grain split-up like you did without adding anything new or changing any behaviors). If, a proposed split-up requires features X and Y to make happen, then we additionally have to discuss here if we want features X and Y and we have to commit to them, because, once we build the layers around them, there's no turning back (it's not like you can easily rearrange the layers after we've gotten committee approval for each individual layer). At the same time, I think it would be difficult to give these features a proper discussion if we're trying to do it at the same time as we're talking about how we should split up pattern-matching.
I'm also not entirely sure why extractor objects needs to be the first step in the series of outlined steps.
It doesn't necessarily need to be the first step, its more of a side path alongside the rest of pattern matching up until (7) above. However, that's not easy to represent in a markdown list.
I do like the idea of having
expr is pattern
as well, as that helps break things up into more steps, and I know there's been others in this proposal expressing a desire for something like that. It does, however, feel a bit odd to be able to introduce a declaration into any location where an expression is allowed, I'm sure that would open up a number of odd edge cases to deal with, which might make the overall complexity of figuring out the spec foris
as difficult as thematch
construct, at which point it didn't really buy us anything to introduceis
beforematch
.
There is prior art we can lean on here. For example, C# allows you to introduce inline variables in a few specific places such as inline out
variables (added in C# 7.0), and pattern matching (1, 2, 3), and has well-defined semantics on scoping for those variables.
Some examples of added complexity:
if (<do declarations here become part of the `if` scope?>) { ... }
In C#, inline variables in the head of an if
are scoped to the if
statement:
if (x is string y && y.Length > 10) { ... }
Here, y
can be used after its declaration in the head of the if
statement, and in the "then" part of the if
statement, but C#'s definite assignment analysis errors on usage along code paths where y
is not initialized (such as in the else
clause, or in an expression like x is string y || y.Length
, where the y
wouldn't have been assigned by the is
). This is essentially the same as TDZ in JS. y
also would be unreachable outside of the if
statement.
for (let i = <Would other variables introduced here persist across all iterations?>; <What happens here?>; <And here?>) { ... }
In C#, inline variable declarations in a for
statement are scoped to the portion of the for
statement in which they were defined:
for (var a = x is string y; /*1*/; /*2*/) /*3*/;
Here, y
is unreachable in (1), (2), and (3). The same is true in these cases as well:
for (var a = /*1*/; x is string y; /*2*/) /*3*/;
for (var a = /*1*/; /*2*/; x is string y) /*3*/;
In a while
statement, inline variables are scoped to the rest of the while
statement head and the loop body, much like if
:
while (x is string y && y.Length > 0) {
Console.WriteLine(y);
}
In a do
statement, inline variables are scoped only to the expression of the while
clause:
do {
// y cannot be used here
}
while (x is string y && y.Length > 10); // y can be used here
class MyClass { x = <what about here?>; y = <can I access something declared in the previous line from here?>; }
In this case, local variable declarations would be scoped only to the expression. This would align with the specification which today reads that the initializer of a field is evaluated as if it were a function, including with a valid this
binding.
I know a lot of the above is abusive of this new power, but it's all still complexity that would need to be discussed and ironed out in the same proposal that we're discussing object/array/primitive matchers. I guess another option is to just forbid declarations with
is
, and iron that out later or never.
This isn't an abuse, it is an intended outcome. The scoping of inline variables in if
and while
above is no different than Rust's if..let
and while..let
statements:
// excerpt from https://doc.rust-lang.org/rust-by-example/flow_control/if_let.html
let number = Some(7);
let letter: Option<i32> = None;
if let Some(i) = number {
println!("Matched {:?}!", i);
}
if let Some(i) = letter {
println!("Matched {:?}!", i);
} else {
// Destructure failed. Change to the failure case.
println!("Didn't match a number. Let's go with a letter!");
}
// excerpt from https://doc.rust-lang.org/rust-by-example/flow_control/while_let.html
let mut optional = Some(0);
while let Some(i) = optional {
if i > 9 {
println!("Greater than 9, quit!");
optional = None;
} else {
println!("`i` is `{:?}`. Try again.", i);
optional = Some(i + 1);
}
}
Here is the same example from Rust, but written with Pattern Matching + Extractors:
// based on excerpt from https://doc.rust-lang.org/rust-by-example/flow_control/if_let.html
const number = Option.Some(7);
const letter = Option.None;
// Rust-style `if let` using destructuring
if (let Option.Some(i) = number) {
console.log(`Matched ${i}!`);
}
// C#-style `if` using `is`
if (number is Option.Some(let i)) {
console.log(`Matched ${i}!`);
}
// Rust-style `if let` using destructuring
if (let Option.Some(i) = letter) {
console.log(`Matched ${i}!`);
} else {
console.log("Didn't match a number. Let's go with a letter!");
}
// C#-style `if` using `is`
if (letter is Option.Some(let i)) {
console.log(`Matched ${i}!`);
} else {
console.log("Didn't match a number. Let's go with a letter!");
}
// based on excerpt from https://doc.rust-lang.org/rust-by-example/flow_control/while_let.html
let optional = Optional.Some(0);
// Rust-style `while let` using destructuring
while (let Option.Some(i) = optional) {
if (i > 9) {
console.log("Greater than 9, quit!");
optional = Option.None;
} else {
console.log(`'i' is '${i}'. Try again.`);
optional = Option.Some(i + 1);
}
}
// C#-style `while` using `is`
while (optional is Option.Some(let i)) {
if (i > 9) {
console.log("Greater than 9, quit!");
optional = Option.None;
} else {
console.log(`'i' is '${i}'. Try again.`);
optional = Option.Some(i + 1);
}
}
Here's more information on the scope of pattern variables in C# 7.0 (though later editions have expanded upon this): https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-7.0/pattern-matching#scope-of-pattern-variables
Oh, that's a nice correlation with C#'s out
params. I didn't think about that.
I will also say, if we do ever with to introduce an is
operator like this, that's allowed to make bindings at any location, I'd much prefer using syntax that requires us to put a let
/const
/var
somewhere in the match syntax on the RHS of is
(unlike the current proposal).
// You almost don't notice that `b` is being declared
if (x is ['a', b]) { ... }
// It's much easier to see that declarations are happening here
if (x is ['a', const b]) { ... }
I think it’s very premature, fwiw, to imagine how the syntax might change as a result of this feedback, given that the champion group hasn’t discussed it yet :-)
Given that we champions are in reasonable agreement that patterns should be usable in several places, I spent some time figuring out what I think the most consistent syntax for integrating them into everything we need is. I have a fuller gist at https://gist.github.com/tabatkins/ee6dfe274e81d76b6069c5ed37d3dcbf#using-matchers, but I'll reproduce them here.
I'd appreciate y'all's thoughts!
New match(){}
expression (unchanged from the current proposal):
match(<val-expr>) {
when(<matcher>): <result-expr>;
when(<matcher>) if(<test-expr>): <result-expr>;
if(<test-expr>): <result-expr>;
default: <result-expr>;
}
Find the first "arm" whose matcher and/or test passes, given the val.
Evaluates to the corresponding result for that arm.
The matcher can produce bindings that are visible within the matcher, within the test, and within the result.
default
arm always matches.
If no arm matches, throws.
New if
form:
if match(when(<matcher>) = <val-expr>) {
// passes if `val` matches the `matcher`,
// and also exposes the matcher's bindings
// to the body of the `if`.
} else if match(when(<matcher>) = <val-expr>) {
// same but with a new val
}
(Can use when(...) if(...)
or even plain if(...)
here, and in later constructs further down.)
Doing this manually with a match expr would be:
match(val-expr) {
when(<matcher>): do {
// body stuff
};
default: match(val-expr-2) {
when(<matcher>): do {
// else-if body stuff
}
}
}
(Do we want an ability to reuse the match()
if you want to do multiple tests on a single value?)
New for
form:
for match(when(<matcher>) of <iterable-thing>) {
// only executes body on items that pass the matcher,
// and exposes the matcher's bindings to the body
}
(Can combine with await
, etc: for match await(when(<matcher>) of <async-iterable>)
.)
Doing it with match() would be:
for(let x of <iterable-thing>) {
match(x) {
when(<matcher>): do {
//body stuff
};
default: continue;
}
}
New while
form:
while match(when(<matcher>) = <val-expr>) {
// Runs val-expr on each iteration,
// breaking when it fails to pass the matcher.
// Exposes the matcher's bindings to the body.
}
Doing it manually with match() would be:
while(let x = <val-expr>) {
match(x) {
when(<matcher>): do {
// body stuff
};
default: break;
}
}
New let
/const
form:
let when(<matcher>) = val;
// Establishes all the bindings from the matcher
// as part of the surrounding scope,
// using `let` or `const` bindings as appropriate.
Doing it manually with match() would be:
match(val) {
when(<matcher>): do {
// all following code in the block context nests in here
};
}
Note that this throws if the match fails, since there's no default
clause.
(This doesn't use the match
keyword like the other forms, as it's not needed and would interfere with the overall structure of the let
statement. if()
clauses will still work here, like the other forms.)
New is
operator
let passes = <val-expr> is when(<matcher>);
Evaluates to true/false if val passes the matcher or not.
Doing it manually with match() would be:
let passes = match(<val-expr>) {
when(<matcher>): true;
default: false;
}
(Is this needed? Is this the best way to spell it? Is this grammatically okay?)
Matchers in function args:
(This is a lot more experimental, many unanswered questions.)
function foo(
when(<matcher>),
<arg2> when(<matcher2>),
<arg3> when(<matcher3>) = <default-expr>,
) {
// Form 1 `when(<matcher>)`
// If argument is passed, and passes the matcher,
// body sees the bindings introduced by the matcher.
// Otherwise, function throws.
//
// Form 2 `arg when(<matcher>)`
// Identical to Form 1,
// except the argument is also bound according to the `arg` bit.
// (This can be an ident or a destructuring pattern.)
//
// Form 3 `arg when(<matcher>) = default`
// If argument is passed, and passes the matcher,
// identical to Form 2.
// If argument is not passed,
// or is passed but doesn't pass the matcher,
// we instead just process the `arg = default` part,
// as normal for existing function arguments.
}
Doing it manually with match()
would be... complicated. Doing it with let when()
would be approximately:
function foo(a, b, c) {
if(a === undefined) throw;
let when(<matcher>) = a;
if(b === undefined) throw;
let when(<matcher2>) = b;
let <arg2> = b;
try {
if(c === undefined) throw;
let when(<matcher3>) = c;
let <arg3> = c;
} catch(e) {
let <arg3> = <default-expr>;
}
// body stuff
}
Extreme stretch goal: integrate this somehow with defining multiple variants of a single named function, and testing args against each in turn, invoking the one that passes the arg tests. I think the way I have this defined above is compatible with extending into that future.
New
if
form:if match(when(<matcher>) = <val-expr>) { // passes if `val` matches the `matcher`, // and also exposes the matcher's bindings // to the body of the `if`. } else if match(when(<matcher>) = <val-expr>) { // same but with a new val }
(Can use
when(...) if(...)
or even plainif(...)
here, and in later constructs further down.)
I don't understand the need for all of the extra syntax here. If we are considering an is
infix expression, then if match
and while match
seem unnecessary and overcomplicated compared to:
if (<val-expr> is <pattern>) {
}
while (<val-expr> is <pattern>) {
}
do {
}
while (<val-expr> is <pattern>);
I also don't see much value in a for match
statement when you can already do for (...) if (...) {}
, which would also work just as well with is
:
for (let x of <val-expr>) if (x is <pattern>) {
}
// which is essentially just
for (let x of <val-expr>) {
if (x is <pattern>) {
}
}
New
let
/const
form:let when(<matcher>) = val; // Establishes all the bindings from the matcher // as part of the surrounding scope, // using `let` or `const` bindings as appropriate.
Doing it manually with match() would be:
match(val) { when(<matcher>): do { // all following code in the block context nests in here }; }
Note that this throws if the match fails, since there's no
default
clause. (This doesn't use thematch
keyword like the other forms, as it's not needed and would interfere with the overall structure of thelet
statement.if()
clauses will still work here, like the other forms.)
Regarding the let
/const
form, I can sympathize with a goal to somehow align more to assignment patterns/destructuring, but this syntax not only feels complex, it collides with Extractors since when
isn't a keyword:
let Foo(x) = val;
New
is
operatorlet passes = <val-expr> is when(<matcher>);
Evaluates to true/false if val passes the matcher or not.
This is another case of a far more complex syntax than is warranted. I think <val-expr> is <pattern>
is more than clear enough without the need to add an extraneous when
.
I also have a gist of various explorations in to pattern matching syntax that can be found here: https://gist.github.com/rbuckton/df6ade207eecad4fc94cedc3aae79ceb. Not everything in that document is something I would propose for adoption, however. If I were to distill it down to the barest and most useful MVP syntax, I would propose the following:
let
/const
patterns)match
expression to test multiple patterns in individual branches, producing an expression resultis
expression to test a single patternI find this approach to require the least boilerplate and cuts reduces the number of moving parts you need to remember. It also has very clear and consistent semantics.
I'll put together a separate gist with more details for this proposal, but it boils down to this:
// simple matching with `is`:
if (val is 0 | 1 | 2) { ... }
if (val is String | Number) { ... }
if (val is Point(0, 0)) { ... }
if (val is { length: 10 }) { ... }
// branch matching with `match`
const y = match (val) {
// `when` clauses
when 0 | 1 | 2: expr;
when String | Number: expr;
when Point(0, 0): expr;
when { length: 10 }: expr;
// `when..if` clauses
when Array if (val.length > 10): expr;
// `if` clauses
if (myGuardFunction(val)): expr;
// `default` clauses
default: expr;
};
// NOTE: 'equiv:' behavior is approximate. `or:` behavior is even more approximate and is only meant to provide
// context to a more complex desugaring in `equiv:`
// pattern syntax: literal constants
val is 0; // equiv: val === 0
val is 1n; // equiv: val === 1n
val is true; // equiv: val === true
val is "hello"; // equiv: val === "hello"
val is null; // equiv: val === null
// pattern syntax: type tests (via custom matchers/`instanceof`)
val is String; // equiv: (%InvokeMatcher%(val, String) !== ~not-matched~)
// or: typeof val === "string"
val is Array; // equiv: (%InvokeMatcher%(val, Array) !== ~not-matched~)
// or: Array.isArray(val)
val is Foo.Bar; // equiv: (%InvokeMatcher%(val, Foo.Bar) !== ~not-matched~)
// pattern syntax: exhaustive object patterns
val is { kind: "move", direction: "up" }; // means: `val` must have properties named "kind" and "direction". If it
// does, `val.kind` must be `"move"` and `val.direction` must be
// `"up"`. If they are, `val` must not have any other own
// enumerable properties.
//
// equiv: (Reflect.has(val, "kind") && val.kind === "move") &&
// (Reflect.has(val, "direction") && val.direction === "up") &&
// (k = new Set(Reflect.ownKeys({ ...val })),
// k.delete("kind"),
// k.delete("direction"),
// k.size === 0)
// pattern syntax: non-exhaustive object patterns
val is { kind: "move", direction: "up", ... }; // equiv: val.kind === "move" && val.direction === "up"
// pattern syntax: exhaustive "array" patterns
val is [0, 1]; // means: `val` must have a `[Symbol.iterator]()` method. If it does,
// get the iterator `it` of `val`. The first resulted yielded by `it`
// must be `0`. If it is, the second result yielded by `it` must be `1`.
// If `it` does not yield a third result, indicate success. However,
// if `it` does yield a third result, close the iterator and indicate
// failure.
//
// equiv: (val !== null && val !== undefined) &&
// (f = val[Symbol.iterator], typeof f === "function") &&
// (it = f.call(val), true) &&
// (r = it.next(), !r.done && r.value === 0) &&
// (r = it.next(), !r.done && r.value === 1) &&
// (r = it.next(), r.done || (it?.return(), false));
//
// or: val[0] === 0 && val[1] === 1 && val.length === 2
// pattern syntax: non-exhaustive "array" patterns
val is [0, 1, ...]; // means: `val` must have a `[Symbol.iterator]()` method. If it does,
// get the iterator `it` of `val`. The first resulted yielded by `it`
// must be `0`. If it is, the second result yielded by `it` must be `1`.
// If it is, close the iterator and indicate success.
//
// equiv: (f = val?.[Symbol.iterator], typeof f === "function") &&
// (it = f.call(val), true) &&
// (r = it.next(), !r.done) &&
// (r.value === 0) &&
// (r = it.next(), !r.done) &&
// (r.value === 1) &&
// (it.return(), true);
//
// or: val[0] === 0 && val[1] === 1
// pattern syntax: negation
val is not 0; // equiv: val !== 0
val is not null; // equiv: val !== null
val is not String; // means: Evaluating the custom matcher `String` against `val` must not
// succeed.
// or: typeof val !== "string"
val is not Array; // means: Evaluating the custom matcher `Array` against `val` must not
// succeed.
// or: !Array.isArray(val)
val is not { length: 10, ... }; // means: `val` must not have a `length` property whose value is `10`.
// or: val?.length !== 10
val is { length: not 10, ... }; // means: `val` must have a `length` property. If it does, the value of
// `val.length` must not be `10`.
// pattern syntax: disjunction
// NOTE: This syntax uses `or` to preserve `|` for bitwise OR since bitwise operators may be useful for numeric
// flags-based enums in the future.
val is 0 or 1; // equiv: (val === 0) || (val === 1)
val is String or Number; // means: Evaluate custom matcher `String` against `val`.
// If that does not succeed, match custom matcher `Number`
// against `val`.
// or: (typeof val === "string" || typeof val === "number")
// pattern syntax: conjunction
// NOTE: This syntax uses `and` to preserve `&` for bitwise AND since bitwise operators may be useful for numeric
// flags-based enums in the future.
val is Array and { length: 10, ... }; // means: Evaluate custom matcher `Array` against `val`.
// if that succeeds, `val` must at least have a `length` property
// that matches `10`.
// pattern syntax: extractors
val is Option.Some(10); // means: Evaluate custom matcher `Option.Some` against `val`.
// If that succeeds, destructure the iterable result into a
// single element that matches `10`
val is Point({ x: 10, y: 20 }); // means: Evaluate custom matcher `Point` against `val`.
// If that succeeds, destructure the iterable result into a
// single element that matches `{ x: 10, y: 20 }`.
// pattern syntax: `let`/`const` (and maybe `var`?) patterns
// NOTE: `let`/`const` patterns are "irrefutable matches" which always succeed.
// `let`/`const` patterns introduce named bindings that can be used elsewhere in the pattern, the body of a
// `match-when` clause, the body of the containing `if` statement (when used with `is` in an `if` head),
// or the body of a `while` loop (when used with `is` in a `while`-loop head).
if (val is Array and { length: let len, ... }) { // `len` scoped to pattern and `if` body
len;
}
const r = match (val) {
when { x: let x, y: let y, ... }: x + y; // `x` and `y` scoped to pattern and clause body
when [let x, let y, ...] if (x > 10): x - y; // `x` and `y` scoped to pattern, `if`, and clause body
}
This leaves room for RegExp patterns, though those could also be pulled out and implemented via extractors:
const SimpleDateRegExp = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/;
if (x is SimpleDateRegExp({ groups: let { year, month, date }})) {...}
This also leaves room for future growth in pattern syntax to accomodate relational patterns, such as:
x is >= 10 and < 100
It's also possible to define match
in terms of is
, conditional expressions, and throw expressions:
const res = match (val) {
when 0: a();
when 1: b();
when 2: c();
};
// equiv. using `is`
const res =
val is 0 ? a() :
val is 1 ? b() :
val is 2 ? c() :
(throw new MatchError("Match not found"));
Though, match
has the benefit of not requiring a temporary variable to cache the expression being matched (so that it isn't reevaluated) and implicitly handles match failure.
I don't understand the need for all of the extra syntax here.
All my when()
s are me being conservative with grammar. If the matchers aren't wrapped, then we'll have to be a lot more careful with syntax in the future. That might be fine! But I wanted to start out safer and relax as needed, rather than designing aggressively and having to pare it back.
It also makes it possible to build in the if()
stuff consistently, while there's no consistent way to do it without the wrapper. (I don't want to create incentives for people to make a "custom matcher" that just takes a boolean and returns it, so they can get arbitrary tests into their patterns.)
Just plain being consistent is also pretty important. If we can chart a simpler, consistent syntax, that's great, but if we pay for simplicity with each syntax being more special-casey it might not be worth it. (It does look like there might well be a nice consistent route with is
, tho, which would be great to explore.)
Also, this aligns the general shape of the block syntaxes with Rust, which isn't at all a requirement, but is nice.
I also don't see much value in a for match statement
One less level of nesting, and consistency with the other blocks.
Regarding the let/const form, I can sympathize with a goal to somehow align more to assignment patterns/destructuring, but this syntax not only feels complex,
Can you explain? Is this just the presence of the when()
wrapper again? Aside from that it's literally as simple as it can possibly be, as it's just the current syntax with destructuring swapped out for a matcher.
it collides with Extractors since when isn't a keyword:
That clashes just because Extractors wants to also be usable as a destructuring pattern, yeah? If it's instead just a matcher pattern this wouldn't be an issue.
I think
is is more than clear enough without the need to add an extraneous when.
It's absolutely clear to a reader at a high level, yeah. I'm not certain it's clear from a grammar perspective, which is why I'm guarding it right now. Happy to be proven wrong.
I also have a gist of various explorations in to pattern matching syntax
Yup, this nearly exactly matches my current gist, assuming we move ident matchers to a dedicated pattern and allow extractors, as I'm suggesting at the end.
I especially like the addition of not
in the pattern. I presume this means that the subtree under a not
does not contribute bindings, tho it can accumulate them during evaluation.
The lack of interpolation syntax is an issue, tho - it means you cannot write a dynamic pattern, and are instead forced to construct the matcher outside of the pattern and then reference it by name. (As you show with the regex example at the end.) That's very unfortunate, but also the lack of interpolation isn't necessary here. You've just eliminated the need for interpolation for common patterns (referencing an existing variable), which is pretty good, but that doesn't prevent us from still having interpolation as needed.
relational patterns
I'm still completely unconvinced we need or want relational patterns; it seems like it would be a big anchor around the neck of parsing forever, with the benefit solely being that you don't need to bind a name in order to compare it. And it requires the RHS to be arbitrary value expression, not a pattern, which is the sort of unexpected mid-pattern context switch we've tried to avoid without explicit signaling/bounds (like the interpolation pattern).
It's also possible to define match in terms of
is
...
That's not possible, unless you're saying that the is
expression will generate bindings that are visible only to the positive leg of the ternary.
That is precisely what I would be suggesting, just like how if (x is Option.Some(let value)) { value; }
would only generate bindings in the "then" branch and not the else
branch.
It's absolutely clear to a reader at a high level, yeah. I'm not certain it's clear from a grammar perspective, which is why I'm guarding it right now. Happy to be proven wrong.
The committee has already balked at the enormous syntax budget of pattern matching. I am concerned this is more likely to further that concern than to address it.
The lack of interpolation syntax is an issue, tho - it means you cannot write a dynamic pattern, and are instead forced to construct the matcher outside of the pattern and then reference it by name. (As you show with the regex example at the end.) That's very unfortunate, but also the lack of interpolation isn't necessary here. You've just eliminated the need for interpolation for common patterns (referencing an existing variable), which is pretty good, but that doesn't prevent us from still having interpolation as needed.
I think having a dynamic pattern in the middle of a pattern is more likely to make the pattern far too hard to read, and would strongly suggest that we don't encourage that approach. Are there any languages that support that today? All major languages with similar pattern matching features that I can think of don't have that notion, though a few are somewhat ambiguous as to what is a reference vs. what is a binding (which let
patterns are intended to solve).
I'm still completely unconvinced we need or want relational patterns;
They're quite useful in C#, i.e.:
string WaterState(int tempInFahrenheit) => tempInFahrenheit switch
{
(> 32) and (< 212) => "liquid",
< 32 => "solid",
> 212 => "gas",
32 => "solid/liquid transition",
212 => "liquid / gas transition",
};
it seems like it would be a big anchor around the neck of parsing forever, with the benefit solely being that you don't need to bind a name in order to compare it.
While that is convenient, yes, it also avoids repetition and avoids jumping out of pattern space and into expression space. I expect you are also more likely to do a relational comparison against a number than you are to do a ===
comparison.
And it requires the RHS to be arbitrary value expression, not a pattern, which is the sort of unexpected mid-pattern context switch we've tried to avoid without explicit signaling/bounds (like the interpolation pattern).
We don't have to allow arbitrary expressions. In C#, relational operators are restricted to constant expressions:
The right-hand part of a relational pattern must be a constant expression. The constant expression can be of an integer, floating-point, char, or enum type.
I find relational patterns far more compelling than RegExp literal patterns, since RegExp literal patterns are easily represented by an Extractor pointing to a variable outside of the pattern. However, I'm not discounting either as potential pattern syntax. I just don't believe either are compelling enough to achieve an MVP solution for pattern matching.
We don't have to allow arbitrary expressions. In C#, relational operators are restricted to constant expressions:
I feel like not being able to compare against the value of a variable would be a very odd and limiting restriction, imo. Especially since you can do so with equality.
I feel like not being able to compare against the value of a variable would be a very odd and limiting restriction, imo. Especially since you can do so with equality.
I'm not saying we have to 100% follow C# semantics. I think restricting the comparison to literal constant values and IdentifierReference would be perfectly acceptable. What I wouldn't want to support is a pattern like when > (foo ? 1 : 0)
. You restrict it to a limited subset of UnaryExpression, such as only allowing prefix +
/-
/~
. Anything else you would have to pull out of the pattern or use an if
.
That said, I think the rabbit hole of a relational pattern design is going too off-topic in this issue, and is something we can discuss offline or in a separate issue.
Regarding the let/const form, I can sympathize with a goal to somehow align more to assignment patterns/destructuring, but this syntax not only feels complex,
Can you explain? Is this just the presence of the
when()
wrapper again? Aside from that it's literally as simple as it can possibly be, as it's just the current syntax with destructuring swapped out for a matcher.
I'm not convinced it's necessary to support deeply nested pattern matching to normal variable declarations. Binding and assignment patterns are more about reaching in to get a value than they are about testing conditions and alternatives, so I don't think match patterns are a good fit.
The primary purpose of an extractor is to extract values from something. Since it serves both value extraction cases and input validation cases, it is well suited to cover both destructuring and matching. Something like let when(10 or String) x = y
only serves to obfuscate what should be an assertion, especially since it looks like a type annotation yet only validates the initialization site, so most of the capabilities of matching would be wasted on a let
/const
.
I don't understand the distinction you're drawing. the extractor Foo(a, b)
is identical in spirt to the interpolation pattern ${Foo} with [a, b]
; anything you'd want to do with one you'd do with the other, and in particular any nesting you'd do with one you'd do with the other. There's nothing semantically separating the two.
let when(10 or String) x = y
isn't the form I was suggesting, fwiw - it's let when(10 or String) = y
, aka identical to the form suggested for if()
and while()
, allowing you to validate and destructure a value via a pattern for the rest of your block scope (rather than requiring you to nest the rest of your function into the block of an if
or similar).
That means that let when(10 or String) = y
would indeed be a useless thing to say, as it doesn't establish any bindings at all; possibly we'd want it to be a syntax error to use this form without having a binding? But let [] = "foo";
is useless-but-valid today, so probably there's no need to guard authors against this. Having a "throw if it doesn't match the pattern" be very easy to express doesn't seem bad, after all.
let when(10 or String) x = y
isn't the form I was suggesting, fwiw - it'slet when(10 or String) = y
, aka identical to the form suggested forif()
andwhile()
, allowing you to validate and destructure a value via a pattern for the rest of your block scope (rather than requiring you to nest the rest of your function into the block of anif
or similar).
let when(10 or String) = y
doesn't make any sense to me as a reader, and I'm definitely not in favor of requiring further nesting for extractors, i.e. let when(Option.Some(let x)) = y
, as it looks like you're extracting a when
function call into a Option.Some
, like you might for Option.Some(Mesage.Move({ x: let x, y: let y })) = y
. The when
syntax is also incompatible with destructuring assignments since when
is not reserved.
If you just want to validate that y
matches a pattern, just do if (y is not 10 or String) throw new MatchError()
, or assert(y is 10 or String)
. There's no need to bring a let
into this.
as it looks like you're extracting a when function call into a Option.Some
It only looks like that if Extractors are part of destructuring as well as pattern-matching. In which case we'd have a parsing conflict anyway. I assume we'd do one or the other.
The when syntax is also incompatible with destructuring assignments since when is not reserved
No, it's fine today; let when() = foo
isn't a valid destructuring pattern. The problem only comes if we wanted to do Extractors, with the current proposed syntax, as destructuring patterns as well.
If you just want to validate that y matches a pattern,
I'm not saying that's the intended use. The purpose of the let when(...) = val
pattern is to pattern-match and create bindings, and also validate while you're at it, identical to what you'd do in a match()
arm, but without requiring you to nest the rest of your function into a match to see those bindings.
If you do write a matcher that doesn't establish any bindings, you still get some use out of it, but it's indeed a little weird. But no reason to disallow it.
as it looks like you're extracting a when function call into a Option.Some
It only looks like that if Extractors are part of destructuring as well as pattern-matching. In which case we'd have a parsing conflict anyway. I assume we'd do one or the other.
I definitely want to use them for both. I want the simple case of const Option.Some(value) = x
for use with ADT enums, but I also want them for pattern matching, just like we will have pattern matching equivalents for ObjectAssignmentPattern/ObjectBindingPattern ({}
) and ArrayAssignmentPattern/ArrayBindingPattern ([]
). I think we must have both.
The when syntax is also incompatible with destructuring assignments since when is not reserved
No, it's fine today;
let when() = foo
isn't a valid destructuring pattern. The problem only comes if we wanted to do Extractors, with the current proposed syntax, as destructuring patterns as well.
This is less about parsing ambiguity and more about the cognitive overhead of having to deal with two different interpretations of foo()
directly nested within each other. I don't find let when()
intuitive at all.
If you just want to validate that y matches a pattern,
I'm not saying that's the intended use. The purpose of the
let when(...) = val
pattern is to pattern-match and create bindings, and also validate while you're at it, identical to what you'd do in amatch()
arm, but without requiring you to nest the rest of your function into a match to see those bindings.If you do write a matcher that doesn't establish any bindings, you still get some use out of it, but it's indeed a little weird. But no reason to disallow it.
I am just not sold on let when
syntax. I don't think it solves the reference vs. binding issue without having to introduce other syntax to disambiguate (i.e. ${}
). It doesn't read well left-to-right like the rest of JS does, and it feels like it's just an attempt to shoehorn in when
everywhere for some kind of thematic consistency that I don't think is necessary. I've based a lot of the syntax I'm proposing off of Rust and C#, but I don't think I've come across any prior art that introduces the level of complexity of let when
et al. Patterns themselves will exhaust our syntax budget and push the limits of what we can get consensus on. I don't think we need to overcomplicate the normal if
, for
, let
, etc. when we can just use match
and is
as our entrypoints into matching.
I think what would be helpful if we're going to compare syntax would be to put together some examples of how real-world code might be adapted to pattern matching in ways that exercise the various syntax options we're proposing here so that we can get an idea of what each might look like in practice.
This is less about parsing ambiguity and more about the cognitive overhead of having to deal with two different interpretations of foo() directly nested within each other. I don't find let when() intuitive at all.
Sure, but my statement there was a response to your comment that it was incompatible. Not liking let when()
is different from let when()
being incompatible with destructuring - it's only incompatible with "destructuring + extractors".
My suggested syntaxes maintained as consistent of a syntax as possible between all the uses of matchers, but there are definitely other options.
I am just not sold on let when syntax. I don't think it solves the reference vs. binding issue without having to introduce other syntax to disambiguate (i.e. ${}). It doesn't read well left-to-right like the rest of JS does, and it feels like it's just an attempt to shoehorn in when everywhere for some kind of thematic consistency that I don't think is necessary.
I... just don't understand what you're saying here. A matcher is, generally, just "better destructuring" + an ability to fail the destructure. There's no semantic difference between [a, b]
as a destructuring pattern and as a matcher (just a mechanical difference of the length check). Both express the exact same semantic - interpreting some object as a iterator, grab the first two items and bind them to a
and b
.
So I have no idea what you mean by this objection. What is the distinction you're drawing, such that let <destructuring-pattern> = x
is fine, but let <matcher-pattern> = x
is not? Why do your arguments there not apply?
I don't think we need to overcomplicate the normal if, for, let, etc. when we can just use match and is as our entrypoints into matching.
Now this I understand. Your suggestion about how to interpret an if(... is ...)
as creating bindings sounds pretty reasonable! And the same for while()
. I don't understand how it would work for for()
or let
, tho. And as much as reasonably possible, I'd like these to all look consistent. (We can change match()
to be consistent with the other blocks, if that ends up being the issue.)
So, in your earlier comment you proposed using is
more broadly, but only applied that to if()
specifically. That is, we got:
let x = match(val) {
when <matcher1>: <expr> /* matcher1's bindings visible here */;
when <matcher2>: <expr> /* matcher2's bindings visible here */;
}
if(val is <matcher1>) {
// matcher1's bindings visible here
} else if(val2 is <matcher2>) {
// matcher2's bindings visible here
}
We'd need to figure out what happens if you write if(val is <matcher1> && val2 is <matcher2>)
, or toss on additional non-is
boolean expressions, but I think this is workable, and easy to read.
But this omitted the other constructs, which I think are useful. while()
seems easy:
while(val is <matcher>) {
// matcher's bindings visible here
}
Note that Rust supports if let
and while let
, meaning there's existence proof that both of these forms are useful.
For for()
, you suggested adapting it to:
for(let x of items) if(x is <matcher>) {
// matcher's bindings visible here (plus the iteration bindings, as normal)
}
This seems reasonable to me. In one way it's less consistent than while()
, but really while()
is a spicy if()
, so having those two be the most consistent is probably right, and having for()
be a little different is probably okay. Maybe we can even make that:
for(let x of items if <test>) {
...
}
which really follows the Python syntax for iteration literals, which is neat!
But I don't see how to adapt this pattern to let
, which seems like it would be an annoying lack. First, let
can destructure, and matching is basically spicy destructuring; I don't see an argument for one that doesn't also argue for the other. Secondly, those bindings you get out of the matcher are genuinely useful; requiring people who want them to instead wrap the rest of their function in a if(is)
construct seems annoying.
(Rust doesn't have the equivalent, but I suspect that's because of the thrown-error situation being less loosey-goosey in Rust. There's nothing special about the bindings situation between let
and a block in Rust otherwise, afaict - they both have well-defined scopes.
But this omitted the other constructs, which I think are useful.
while()
seems easy:while(val is <matcher>) { // matcher's bindings visible here }
Note that Rust supports
if let
andwhile let
, meaning there's existence proof that both of these forms are useful.
I wasn't discounting while
et al, I even referenced it specifically in my comment above. Since is
is just a binary expression, it could be used in if
, while
, for
, conditionals, etc. Its versatility is the reason I proposed it.
Maybe we can even make that:
for(let x of items if <test>) { ... }
which really follows the Python syntax for iteration literals, which is neat!
I don't think moving the if
into the for
is necessary. You mention Python, but that syntax is unique to comprehensions and conditionals. There isn't an inline if
in a normal for
statement in Python.
But I don't see how to adapt this pattern to
let
, which seems like it would be an annoying lack. First,let
can destructure, and matching is basically spicy destructuring; I don't see an argument for one that doesn't also argue for the other. Secondly, those bindings you get out of the matcher are genuinely useful; requiring people who want them to instead wrap the rest of their function in aif(is)
construct seems annoying.
If you want to introduce bindings, you could just do:
let { x, y } = val is Point(let x1, let y1) ? { x: x1, y: y1 } : null; // will throw if fails.
or just use is
:
// x and y are in scope and in TDZ and are only initialized if the pattern matches.
val is Point(let x, let y);
console.log(x);
Or even better:
assert(val is Point(let x, let y));
I don't think moving the if into the for is necessary. You mention Python, but that syntax is unique to comprehensions and conditionals. There isn't an inline if in a normal for statement in Python.
Yes, I know, but we're not Python. My point is that if if()
and while()
can usefully use matchers, it would be weird for for()
(the more common of the looping structures) to not be able to.
That said, I'm no certain what your objection is here. Are you arguing for for(...) if(...) {...}
? Or for not integrating matchers with for()
at all?
If you want to introduce bindings, you could just do:
I'm sorry, but I need to make sure: was this suggestion made in earnest? Because it is extraordinarily verbose and circuitous for such a simple and straightforward use-case.
Again I ask: what is separating destructuring and matching that makes let <destructurer> = x;
acceptable and readable, but let <matcher> = x
bad?
or just use is:
While this might be workable, it gives us a totally different syntax pattern than let ...;
, which is unfortunate given how the other constructs look like their normal selves. It does let us do let
/const
as the variable binding keywords without questions about what they mean if the let/const statement disagrees, tho. (But there are other possible keywords for this, like as
, which we could also use, defaulting to a let
semantic but allowing the let
/const
/var
statement override that.)
Edited Note: I've injured my wrist quite badly. Some context is missing here, it will be filled in later (in a week or so hopefully).
As mentioned on matrix -- and in past meetings of the champion group, i believe this proposal is too complex in its current form, and as a champion i have raised this and the epics concept as a way to alleviate this. When this appeared on the agenda for stage 2, i was not informed. I have rushed to put this together, it is only an idea, or a write up of one, of how this could be done: https://github.com/codehag/pattern-matching-epic
and here, is a suplimentary document. https://docs.google.com/document/d/1dVaSGokKneIT3eDM41Uk67SyWtuLlTWcaJvOxsBX2i0/edit
There are many forms of simplification and layering that are possible here. this is not the only one. Perhaps this is too fine grained. As i wasn't informed of this moving to stage 2 (likely because i was ill at the time), its clear that my contribution has been small so i've also stepped down as a champion.
sorry i can't say more now as its quite hard to type.