-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
We should reframe the approach here #281
Comments
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 |
I'm gonna paste my reply on Matrix.
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.
At the first glance, this makes me think of @rbuckton 's unapply proposal. I'm much in favor of that form in the past, Here is a feedback from a normal JavaScript developer:
|
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:
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 // 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 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 I'm also not sure what 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 What about cases like this: const x = Math.floor(Math.random() * 1000);
const y when isEven(x); What is the expected value of Please, take whatever time you need to respond. |
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 I have the same feeling about the Pipeline proposal, which can be split into Partial Function Application ( I see the same idea in Yulia's design so I like it.
LOL I agree
Oh No, please don't. See my reply at tc39/proposal-do-expressions#75
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:
and
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 should have been clearer here: this isn't a perfect analogy. The only thing set here is if
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.
happy with "layered proposal", thats a good suggestion
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 |
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 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 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
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.
good point, can be a syntax error possibly
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 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.
While 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
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, // 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;
}
};
Introducing an 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 Introducing // 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
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 would be more partial to explicit binding declarations via inline const url = "http://xyz"
match (x) {
when ({ status: 500, destination: let url }): handle(x, url)
} I still find the
This restriction concerns me as it explicitly puts
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 // 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 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 @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 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 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:
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 With a powerful core in place, we can move features like the following into add-on proposals:
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) |
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 // 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`);
} |
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 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. |
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:
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 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 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 |
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.
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.
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:
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. |
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 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 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. |
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. |
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
In C#, inline variables in the head of an if (x is string y && y.Length > 10) { ... } Here,
In C#, inline variable declarations in a for (var a = x is string y; /*1*/; /*2*/) /*3*/; Here, for (var a = /*1*/; x is string y; /*2*/) /*3*/;
for (var a = /*1*/; /*2*/; x is string y) /*3*/; In a while (x is string y && y.Length > 0) {
Console.WriteLine(y);
} In a do {
// y cannot be used here
}
while (x is string y && y.Length > 10); // y can be used 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 isn't an abuse, it is an intended outcome. The scoping of inline variables in // 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);
}
} |
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 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. |
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 |
I don't understand the distinction you're drawing. the extractor
That means that |
If you just want to validate that |
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.
No, it's fine today;
I'm not saying that's the intended use. The purpose of the 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 definitely want to use them for both. I want the simple case of
This is less about parsing ambiguity and more about the cognitive overhead of having to deal with two different interpretations of
I am just not sold on 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. |
Sure, but my statement there was a response to your comment that it was incompatible. Not liking My suggested syntaxes maintained as consistent of a syntax as possible between all the uses of matchers, but there are definitely other options.
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 So I have no idea what you mean by this objection. What is the distinction you're drawing, such that
Now this I understand. Your suggestion about how to interpret an |
So, in your earlier comment you proposed using 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 But this omitted the other constructs, which I think are useful. while(val is <matcher>) {
// matcher's bindings visible here
} Note that Rust supports For 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 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 (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 |
I wasn't discounting
I don't think moving the
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 // 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)); |
Yes, I know, but we're not Python. My point is that if That said, I'm no certain what your objection is here. Are you arguing for
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 |
While this might be workable, it gives us a totally different syntax pattern than |
Is |
I described the behavior in #281 (comment) (and alluded to it #281 (comment) with talk of Python's list comprehensions). It would skip the item. (I didn't grok that |
Just carrying some conversation over from the chat room, because Ron needs to be on vacation and stop responding but this is good to store for later response: (from @rbuckton )
This is simply wrong - Matchers can just fail a little earlier - |
Since the immediate reaction to my suggested syntax changes a few days ago was dislike of the increased verbosity, here's a new draft. Quick summary:
The function arg syntax is the one I'm still most unsure about, it could probably still use some fiddling for maximum readability. Possibly the The rest, tho, are I think minimal, clear, and importantly, very consistent and predictable. |
Been fiddling with my draft proposal for "matchers everywhere" more, based on feedback from several people. Major changes from above:
|
Why would custom matchers be required to return their values as an iterator? That sounds very confusing. |
The return value can be an array or something; it just needs to be iterable. (A benefit to doing this, besides the confluence in syntax, is that we no longer need the special "result object". If you return an iterable, that's a successful match; we can also let you return true/false to indicate a successful or failed match. Any other value would be a runtime error.) |
Maybe I'm confused. Why would |
That's intrinsic to the syntax? It's always been the case, both in the Extractors proposal, explicitly, and in all of my adaptations of extractors into matchers. ( |
I hadn't realized that implication, and to me that's a very very strong argument against considering that to be the desugaring. Forcing the iterator protocol when it's not absolutely necessary seems like a very unwise idea. |
I have no idea what else the desugaring could possibly be. And this exact desugaring is used by other langs already, like Python's class matchers. |
I would expect it to only ever accept one argument, and have that be the pattern tested - so that if you wanted array iterator syntax, you'd type that. |
That loses the very nice syntax mirroring of function-call/construction <=> matcher pattern, which we have with array and object literals. Again, this sort of matcher syntax is accepted by many existing langs in their matcher patterns, for this reason - that syntax mirroring is pretty attractive. For example, given a Forcing that to be |
For code let Foo(a, b) = expr
|
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.
The text was updated successfully, but these errors were encountered: