Skip to content
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

wip: sketching out idea in examples #140

Closed
wants to merge 1 commit into from
Closed

Conversation

zkat
Copy link
Collaborator

@zkat zkat commented Jun 6, 2019

This is an initial sketch of an idea that came to mind tonight.

tl;dr: Instead of prefixing each clause with when, use let, const, and var. I'm hoping that this achieves two things: 1. make it clear that these clauses use the same/similar destructuring rules as regular assignment; 2. it allows constants, lets, and vars so we know how the bindings are actually supposed to work.

I'm almost certain someone has proposed something like this in the past, too, but I honestly don't remember where or when. I definitely know const bindings were requested in the past, though.

Let me know what you think!

@noppa
Copy link

noppa commented Jun 6, 2019

The examples don't include var. Would var-prefixed clauses hoist the declaration?

@zkat
Copy link
Collaborator Author

zkat commented Jun 6, 2019

@noppa that's what I'm thinking it should do, yep. I'm not sure if var should be allowed, though, but I don't have a solid argument why not

@remmycat
Copy link

remmycat commented Jun 7, 2019

One problem i have with this is, that the when-clauses aren't exactly destructuring. Making it look like the same would rather confuse people i think?

Consider the example from the current readme:
when {status: 200}
This means it will execute, when status is 200

However, in destructuring the colon is used to assign a new name where const {status: 200} is gonna throw an error, because 200 is not a valid name.

@ljharb
Copy link
Member

ljharb commented Jun 7, 2019

That definitely seems to be the confusion for me - using const/let/var means it should work like destructuring, but destructuring only provides a facility to create bindings, not to match.

@zkat
Copy link
Collaborator Author

zkat commented Jun 11, 2019

I want to argue that the entire -premise- is to make everything look just like regular destructuring assignment, with a few extra rules for literal-matching that don't exist at all in regular destructuring.

@chee
Copy link

chee commented Jun 12, 2019

i like this!

if there is confusion between destructuring and matching, is it too unsanitary (or impossible) to use the = syntax?

let {status = 200, headers = {'Content-Length' = s}}

@zkat
Copy link
Collaborator Author

zkat commented Jun 12, 2019

That would directly conflict with default parameter syntax, which is supported by case.

@chee
Copy link

chee commented Jun 12, 2019

am i right in this:

{status: 200, headers: {'Content-Length': s}}
   _______^                         ______^
  |                                |
matching                        assigning

and does that mean there's no way to do match against a scalar defined elsewhere:

{status: HTTP_OK, headers: {'Content-Length': s}}

without using the if syntax?

(i see now this isn't different from when there was when)

@zkat
Copy link
Collaborator Author

zkat commented Jun 12, 2019

That is correct. There's talk of a pin operator, the way Elixir does it, but that's a syntax grab I don't want to do. Note that other languages have this same behavior and expect guards for this use-case, such as Rust, and Elixir (unless you use an explicit pin (^) operator). Erlang does assignment IFF the variable doesn't already exist, which I'd consider fairly confusing behavior for JavaScript.

console.log(`size is ${s}`),
when {status: 404} -> {
let {status: 404} -> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let {status: 404} -> {
let {status: 404} ->

I think this { is a left over right?

@littledan
Copy link
Member

I'm a big fan of this proposal. I'd say, merge it! This PR makes it more clear that we're creating an inner binding, and it reduces the number of keywords in the language.

I think the analogy with destructuring is really important, but that's not because of the keyword let vs when (remember that there's also keyword-less destructuring assignment, and destructuring in parameters and catch blocks), but rather because pattern matching looks like an LHS at all. This proposal differs from destructuring in semantics because the case keyword and -> token differentiate it, and because the destructuring semantics simply don't work to explain basic pattern matching cases.

@phaux
Copy link

phaux commented Jun 21, 2019

But it's not the same, because destructuring works with any iterator, whereas pattern matching was supposed to work only with arrays...

@zkat
Copy link
Collaborator Author

zkat commented Jun 21, 2019

@phaux it does! The new version of the proposal operates on iterators, the same way destructuring does.

@zkat zkat marked this pull request as ready for review June 21, 2019 15:25
Copy link
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think this is a good idea; the confusion with variable assignments, including destructuring and defaults, isn’t worth it (unless I’m misunderstanding and they’re exactly the same).

I think the when is an important signal at the start of the statement that this is a different thing - a pattern match.

@zkat
Copy link
Collaborator Author

zkat commented Jun 21, 2019

@ljharb it works just like destructuring. The whole idea here is that if you know one, you know the others, and in that case, I do prefer the similarity. Also, would removing var from this help at all?

@ljharb
Copy link
Member

ljharb commented Jun 21, 2019

@zkat including default args? assuming so:

  • removing var would definitely help, because of scoping - it wouldn't make sense to have these variables available outside the overarching match construct.
  • however, since all of the const/lets are siblings, i'd expect them all to be defined. let and const hoist to the top of the scope created by their enclosing curly braces, so:
case (res) {
   let {status: 200, headers: {'Content-Length': s}} ->
     console.log(`size is ${s}`),
   let {status: 404} -> {}
}

would be a TDZ violation for declaring status twice, and i'd think it'd be confusing to not get that TDZ error.

@stephenh
Copy link

stephenh commented Jun 21, 2019

Fwiw as a lurker, I think the "oh, this works just like destructuring" is great in terms having a combined/single mental model across variable declaration/assignment and pattern match. I.e. if that was not the case, I could see users/new users constantly getting tripped up at "these [the destructuring syntax within const/let/when] look the same...but aren't..."

That said, while I see the elegance of avoiding a new keyword, I'm tempted to think the when keyword is still useful (given its a keyword, and I think new language users are very keyword-driven), that "this is really not the same thing as a variable declaration/assignment".

I.e. it starts a new block, and whenever the next "when" happens means this "when" stops, which is also unlike let/const/etc, and I don't know that just the -> is a big enough hint for that big of a semantic change from "single line variable assignment" --> "this is a block of scope" (which are denoted about other "control-flowy" keywords like if/while/etc.).

@ljharb
Copy link
Member

ljharb commented Jun 21, 2019

Using when let and when const might work well though - then it would exactly mirror people's expectations, and we'd be able to teach that when creates a new lexical scope.

@sendilkumarn
Copy link

let statements here makes it feel like an assignment. I like the idea of when let | when const.

@mAAdhaTTah mAAdhaTTah mentioned this pull request Jun 24, 2019
2 tasks
@goodmind
Copy link

Looks really strange, aren't all bindings constant in pattern matching?

@jonathanmarvens
Copy link

As a lurker, going with when let/when const over the potentially confusing reuse of let/const feels like a good compromise 👌🏾

@littledan
Copy link
Member

I'm skeptical of when let. It's pretty wordy for something that will be repeated many times. And I don't think including the when changes whether people will make the same analogy with destructuring. Let's figure out one keyword that will be acceptable (I still like let/const for it).

@igl
Copy link

igl commented Jun 26, 2019

Why would case do a destructuring assignment in the first place? I never thought that it would work like this before reading this.

Matching against variables would be way more beneficial and "I am constructing a literal to match against" is a much simpler concept and easy to explain.

The inclusion of the if statement is already a foreign feature straight out of elixir.
It's nice because Elixir is the pattern match king... but that ship already sailed in js land.

@goodmind
Copy link

@igl I don't understand what you want, current pattern matching makes sense, because it IS familiar to other languages

@czabaj
Copy link

czabaj commented Jun 27, 2019

For me there is too much differences to destructuring assignment that using let let / const binding would potentially lead to more confusion.

One case is the TDZ error mentioned by @ljharb above.

Second case is that desctructuring of potentially undefined value will not throw an error, but will be skipped as NOT MATCH.

Next, you probably don't want to support default values with pattern matching, because it will be confusing, people might want to try

case (res) {
   let {status: 200, headers: {'Content-Length': s} = { 'Content-Length': 128 }} ->
     console.log(`size is ${s}`),
   let {status: 404} -> {}
}

Or even worse

case (res) {
   let {status: 200, headers: {'Content-Length': 50} = { 'Content-Length': 128 }} ->
     console.log(`size is ${s}`);
}

Support for default values like this could break matching or lead to unreadable code.

And there are probably more differences I could not find right now.

So if we discuss, weather using const/let or when, either way, you must explain to the crowd, that pattern matching acts almost like object destructuring, but IMHO it is better to keep it foreign concept with match ... when and explain the similarities, than tell them that it is (almost) the same and than explain all the hard to catch differences.

@noppa
Copy link

noppa commented Jun 27, 2019

desctructuring of potentially undefined value will not throw an error

Hol' up, this could be (ab)used to get optional destructuring 😅 tc39/proposal-optional-chaining#74

// Before
var bar = obj?.foo?.bar;
var baz = obj?.foo?.baz;

// After
case (obj) { var { foo: { bar, baz } } -> {} }

I'm not sure if that's brilliant or terrifying.

@phaux
Copy link

phaux commented Jun 27, 2019

Hol' up, this could be (ab)used to get optional destructuring sweat_smile tc39/proposal-optional-chaining#74

I think that was the point from the beginning

const { bar, baz } = case (obj) {
  when { foo: {bar, baz} } -> ({ bar, baz })
}

@noppa
Copy link

noppa commented Jun 27, 2019

@phaux Right, yeah, well that's better than the var-hack.
I think you'd need a default case, though

when _ -> ({ bar: undefined, baz: undefined })

for the case where foo is not there. Kinda starts loosing its succinctness in favor of the original "manual" destructuring example.

@goodmind goodmind mentioned this pull request Aug 14, 2019
@RobertLowe
Copy link

when (let|const), with let and const as optional, would default to const without, so:

when {text} -> ({foo:text})
when let {text} -> {
    text += "-bar";
    return ({foo:text})
 }
when const {text} -> ({foo:text})

would all be valid forms

@aleen42
Copy link

aleen42 commented Apr 20, 2021

In my opnion, it seems something like it.let(block) in Kotlin, or with statements where we can call a specified function block with a context.

For example, the snippet mentioned above:

case (res) {
   let {status: 200, headers: {'Content-Length': s}} ->
     console.log(`size is ${s}`),
   let {status: 404} -> {}
}

Seems like a pattern here:

with (res) {
  _.let(res, ({status, headers: {'Content-Length': s}}) => status === 200 && console.log(`size is ${s}`));
  _.let(res, ({status}) => {});
}

If considering short-circuits:

with (res) {
  // case 200
  _.let(res, ({status, headers: {'Content-Length': s}}) => status === 200 && console.log(`size is ${s}`))
      // other cases
      && _.let(res, ({status}) => {});
}

@ljharb
Copy link
Member

ljharb commented Apr 21, 2021

The proposal has been significantly updated in #174; this PR seems no longer relevant, given that we've intentionally separated patterns from bindings.

@ljharb ljharb closed this Apr 21, 2021
@ljharb ljharb deleted the zkat/var-specifiers branch April 21, 2021 05:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.