Skip to content

We should reframe the approach here #281

Closed

Description

@codehag

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.

Activity

  1. ljharb commented on Jul 18, 2022

    @ljharb
    Member

    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.

  2. codehag commented on Jul 18, 2022

    @codehag
    Author

    thats not true.

  3. ljharb commented on Jul 18, 2022

    @ljharb
    Member

    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”

  4. Jack-Works commented on Jul 18, 2022

    @Jack-Works
    Member

    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.

  5. rkirsling commented on Jul 18, 2022

    @rkirsling
    Member

    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.)

  6. rbuckton commented on Jul 18, 2022

    @rbuckton
    Member

    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.

  7. Jack-Works commented on Jul 19, 2022

    @Jack-Works
    Member

    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 tc39/proposal-do-expressions#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!
  8. codehag commented on Jul 19, 2022

    @codehag
    Author

    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.

  9. changed the title [-]This needs to be simplified[/-] [+]We should reframe the approach here[/+] on Jul 19, 2022
  10. codehag commented on Jul 19, 2022

    @codehag
    Author

    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.

  11. ljharb commented on Jul 19, 2022

    @ljharb
    Member

    @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:

    1. the proposal would be better designed (by being less overwhelming to understand to delegates and implementors, and thus receiving more scrutiny and better feedback) if it were conceptually split into different layers, regardless of whether they ship simultaneously or not.
    2. The champion group should reevaluate whether bindings and patterns should be conflated (while still allowing for sugar for the common case to avoid repetition) to avoid user confusion from varied approaches to reading and understanding the syntax.
    3. @codehag personally reads the bindings as similar to destructuring assignment, which in the case of separation would mean bindings come first, before patterns; I personally read them as similar to a destructuring in a function or catch block signature, which would mean patterns come first, before bindings - while subjective matters of perspective may never be resolved, we need to discuss and explore this thoroughly to ensure we're designing the best possible language feature.

    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.

  12. littledan commented on Jul 19, 2022

    @littledan
    Member

    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.

  13. treybrisbane commented on Jul 20, 2022

    @treybrisbane

    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).

  14. 38 remaining items

  15. CadenP commented on Mar 31, 2023

    @CadenP

    for(...) if(...) {...} is currently usable syntax, capable of skipping items that don't pass the condition in the if without breaking the loop. while(<condition>) breaks the loop, and for(...; <condition>; ...) breaks the loop as well.

    Is for(let x of items if <test>) supposed to break the loop or skip the item?

  16. tabatkins commented on Mar 31, 2023

    @tabatkins
    Collaborator

    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 for(...) if(...) {...} was already valid syntax, but you're right, it is. Phew, braceless blocks are really a trip sometimes.)

  17. tabatkins commented on Mar 31, 2023

    @tabatkins
    Collaborator

    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 )

    The let { x, y } = example is verbose, yes, but uses what could otherwise be regular js in the initializer.
    In a way it's meant to illustrate that a let statement isn't a good fit for pattern matching itself, because matching is conditional and let is not.
    The closest you get to a useful example is the assert one, because you need to validate the condition was successful.

    This is simply wrong - let is not unconditional. (Or at least, let with destructuring isn't.) let {foo} = undefined; throws a runtime error! let {foo} = {} executes successfully (binding undefined), but let {foo: {bar}} = {}; again throws. The destructuring behavior matches what you'd get if you instead wrote a series of assignments, each digging into the value with an appropriate dotted/bracketed path, and that can def fail.

    Matchers can just fail a little earlier - let when {foo} = {}; would throw because the match failed. But I don't think that's a difference that makes a difference here.

  18. tabatkins commented on Mar 31, 2023

    @tabatkins
    Collaborator

    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:

    • Matchers are identical to what's in the repo today, except I added "predicate matchers" (if(<boolean-expr>)) and extractor matchers (foo(...) and foo{...}, which are shorthands for ${foo} with [...] and ${foo} with {...}).

    • Slightly simplified match() - it still uses a when prefix on each arm, but it's just a keyword rather than a wrapper, to be consistent with the additional stuff. Also dropped the if() part, since predicate matchers exist now.

       let x = match(val) {
       	when <matcher>: <return-val>;
       	when <matcher>: <return-val>;
       	default: <return-val>;
       };
    • Added <val> is <matcher> binary operator. Evaluates to true/false based on value matching the matcher.

    • Extended var/let/const, if(), for(), while(), catch(), and function args with matchers, using a much lighterweight syntax:

       let when <matcher> = x; // throws if match fails, otherwise exposes bindings.
       	// bindings use the specified semantic (var, let, or const).
       	// In all other locations they use "let" semantics.
       if(when <matcher> = x) {...} // executes body if match succeeds, with bindings.
       for(when <matcher> of x) {...} // executes body if match succeeds, with bindings.
       while(when <matcher> = x) {...} // executes body while match succeeds (breaks when it fails)
       catch(when <matcher>) {...} // executes body if catch succeeds, goes to next catch() if not
       	// re-throws if all catches fail
      
       function foo(when <matcher>, x when <matcher>, y = default when <matcher>) {
       	// `when <matcher>`: throws if matcher fails, exposes bindings to body
       	// `x when <matcher>`: throws if matcher fails, exposes bindings to body, also binds arg to x
       	// `y = default when <matcher>`: exposed bindings to body, also binds arg to y. 
       	//		Doesn't throw on failure, just binds y to default instead.
       	// `x` and `y` here can be destructuring patterns, as usual for arguments.
       }

    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 is operator could be fiddled with more, too - not married to the name, and the argument order is opposite how matchers are used elsewhere.

    The rest, tho, are I think minimal, clear, and importantly, very consistent and predictable.

  19. tabatkins commented on Apr 7, 2023

    @tabatkins
    Collaborator

    Been fiddling with my draft proposal for "matchers everywhere" more, based on feedback from several people.

    Major changes from above:

    • defined how matchers can be used in destructuring patterns, as suggested by Jordan.
      • this replaces the special handling of let/const/var, for(), and function args - their behavior falls out of the fact that they allow destructuring. (if(), while(), and catch() still require some special handling.)
    • added <ident> when <matcher> to explicitly handle "name this chunk and test it more", in a consistent way with the destructuring syntax
    • switched regex matchers to /foo/ when <matcher> for consistency, and dropped the named-capture-groups bindings for simplicity. (You can get standard capture group with when [_, first, second], or named groups with when {groups: {groupname}}
    • Separated ${} syntax syntactically into "test against a variable/dynamic value" (written as ${...}) and "invoke a custom matcher" (written as ${...}(<matchers>), and interpreted identically to the foo(<matcher>) syntax). Now custom matchers are required to return their values as an iterator.
      • As part of this, removed ${...} with <matcher> entirely. It's no longer needed.
    • Defined that foo() and ${foo}() (empty arglists) don't check the custom matcher's result value, just whether it succeeded or failed.
  20. ljharb commented on Apr 7, 2023

    @ljharb
    Member

    Why would custom matchers be required to return their values as an iterator? That sounds very confusing.

  21. tabatkins commented on Apr 14, 2023

    @tabatkins
    Collaborator

    Foo(a, b) matches the result of invoking Foo[Symbol.matcher] against [a, b]. If we match up interpolated matchers as ${Foo}(a, b), as I suggest above, then that's also matching the result against [a, b].

    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.)

  22. ljharb commented on Apr 14, 2023

    @ljharb
    Member

    Maybe I'm confused. Why would Foo() matching syntax ever invoke the iterator protocol? (separate from user code doing so, ofc)

  23. tabatkins commented on Apr 14, 2023

    @tabatkins
    Collaborator

    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. (Foo(a, b) has always been equivalent to ${Foo} with [a,b].) I'm not sure how you think when Foo(a, b) would work, otherwise.

  24. ljharb commented on Apr 14, 2023

    @ljharb
    Member

    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.

  25. tabatkins commented on Apr 14, 2023

    @tabatkins
    Collaborator

    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.

  26. ljharb commented on Apr 14, 2023

    @ljharb
    Member

    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.

  27. tabatkins commented on Apr 14, 2023

    @tabatkins
    Collaborator

    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 Point object with a constructor like new Point(x, y), giving it a custom matcher that returns the x and y values, and is usable like when Point(x, y): Math.hypot(x, y); is pretty sweet.

    Forcing that to be when Point([x, y]) is a lot less attractive, and brings up reasonable questions like "well what're the parens for, then?".

  28. Jack-Works commented on Jul 20, 2023

    @Jack-Works
    Member

    Maybe I'm confused. Why would Foo() matching syntax ever invoke the iterator protocol? (separate from user code doing so, ofc)

    For code

    let Foo(a, b) = expr

    Foo[@@unapply](expr) will return an array [aVal, bVal], then destructing is happening (a, b) therefore @@iterator is called

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions