tc39 / proposal-pattern-matching

Pattern matching syntax for ECMAScript
https://tc39.es/proposal-pattern-matching/
MIT License
5.5k stars 89 forks source link

Why is the separator an arrow instead of a colon? #119

Closed demurgos closed 5 years ago

demurgos commented 6 years ago

Hi, I do not intend to bikeshed on the decision but I am just curious why the proposal uses -> to separate the pattern from the clause body instead of :. Instinctively, I expected :. It would be consistent with switch/case and avoid introducing a new syntax. Would a colon introduce an ambiguity in the grammar? Or was it specifically avoided to be different from switch/case? I feel that when is enough to avoid ambiguity but maybe it would break some other proposal?


Edit: @grundiss posted a comment I like: he proposes to simply use braces instead of a separator.

PinkaminaDianePie commented 6 years ago

As for me, Instinctively is a bit subjective thing. -> is used in other functional languages for pattern matching (haskell for instance), and since pattern matching comes from FP languages I instinctively expect -> as a separator here.

kaibyao commented 6 years ago

I'd actually have to agree with OP on this one here; it's true that -> is used for pattern matching in other languages, but I'm not sure it makes sense within JavaScript.

As @demurgos mentioned, JS developers are already used to using colons for do...while loops and switch statements; remembering how to use pattern matching would be easier from that standpoint. -> is another syntax to remember, and may cause confusion when people attempt to use arrow function fat arrows (=>) for pattern matching.

The advantage I can think of with using -> instead of : is that the arrow would explicitly denote that a pattern match is taking place, as it’s not used anywhere else in the language. Because : is already used for other things, using : for pattern matching might actually make code harder to read, as you would have to figure out whether you're looking at a do...while loop, a case, or a pattern match.

I guess from that standpoint, you would be choosing between productivity vs. ease of adoption.

demurgos commented 6 years ago

I initially posted my comment after reading the notes of the July's TC39 meeting. They have a section discussing syntax.

I have experience with languages using pattern matching (Ocaml, Scala, Rust) and saw their influence when choosing an arrow. My comment should have been better phrased: why was a new syntactical element chosen in the context of existing Javascript? The ML languages that served as inspiration don't have switch/case, only pattern matching. But JS already has switch/case, and pattern matching is just "switch/case on steroids". Since both are similar, I expect them to have similar syntax. That's why I consider : to be the intuitive choice: bring the semantics from other languages but keep the JS syntax consistent.

This parallel between switch/case and when is really important IMO. That's why I consider that introducing new kinds of tokens should have a stronger reason than imitating other languages: grammar ambiguity, limitations of the engines, spec issues, etc.

ljharb commented 6 years ago

It’s not just switch/case on steroids, and i don’t want any overlap in the naming between the two, nor do i want anyone to ever have to use or learn about switch/case ever again. Switch statements are terrible and should never be used, and i want to see this new proposal be a distinctly googleable and understandable construct that can be consistently recommended in their place.

demurgos commented 6 years ago

There are some differences (no fall-through, expression instead of statement, destructuring) but the core idea remains the same: select one out of many branches based on a value. I agree that this proposal is strictly superior to the current switch/case and should be teached instead, but you can't deny that there are similarities. Except for the fall-through behavior, you can always replace a switch/case by a case/when: it's a more general concept.

Just to be clear: my issue is extremely minor and I don't have any strong feelings about it. Whatever the syntax, I'll be happy if this lands.

ScottRudiger commented 6 years ago

i don’t want any overlap in the naming between the two

Because I never use switch statements anymore, I was initially confused for a second, thinking that pattern matching happens inside of a switch statement. My confusion was due to the use of case ().

I'm guessing case was used because it's an already available keyword and its meaning is close to its usage here (good reasons), but is there any other option?

FireyFly commented 6 years ago

The keyword case also has precedent in functional languages with no concept of switch statements (ML, Haskell, lisps, etc), so the choice isn't purely driven by it already being a reserved word/being analogous to switch-case.

A little syntax comparison might be useful (I'm not super familiar with these languages, so this is off of a quick internet search).

(* Standard ML *)
case EXPR of
  PAT1 => RES1
| PAT2 => RES2
| ...
-- Haskell
case EXPR of
  PAT1 -> RES1
  PAT2 -> RES2
  ...
grundiss commented 6 years ago

Speaking of syntax, switch...case always irked me with two things:

  1. Why using semicolon if you can define block like this:

    switch(response.status) {
    case 404 {
        console.log('Page not found');
    }
    }

    So, in fact I don't think any separator is needed.

  2. Why statement next to the case is not wrapped in parentheses? For me case always looked like something very-very similar to if, and if always has expression it relays on in parentheses. So my perfect switch...case would look like this:

    switch(response.status) {
    case(404) {
        console.log('Page not found')
    }
    }

But, yeah, since we are talking about case...when, not switch...case, may I suggest syntax like this:

case(response) {
    when({status: 404}) {
        console.log('Page not found');
    }
}
ghost commented 5 years ago

Yeah, I think if the face of JS shall grow towards a clear character, it needs to go backwards in complexity.

const condition = true

const data = {
  condition
}

function express() {
  return 1 + 1
}

if(condition === true) {
  express()
}

switch(condition) {
  case true {
    express()
  }
}

case(data) {
  when {condition} {
    express()
  }
  when _ {
    express()
  }
}

pipe(data) {
  into express
  into express(_, 7)
  into var piped
}

Scroll down, if you’d like to see a comparison.

Note: this is no concrete suggestion, but rather a contribution into a direction.

const condition = true

const data = {
  condition
}

function express() {
  return 1 + 1
}

if(condition === true) {
  express()
}

switch(condition) {
  case true:
    express()
    break
  default:
    break
}

case(data) {
  when {condition} -> {
    express()
  }
}

const piped = 
  data
    |> express(#)
    |> express(#, 7)
lukescott commented 5 years ago

When I see -> I think function. It is too similar to =>. With => being a bound function, I would expect -> to be an unbound function, even if that syntax never happens.

From the few comments from @ljharb that I have seen, it seems like the design decisions around this syntax is to distance it from switch/else as much as possible. I have seen comments of "it is easier to each this way". I disagree. Familiar syntax is much easier to teach and understand. And even though some people have a hate for switch/case that rivals eval and goto, switch/case really isn't that bad.

In modern languages like Go and Swift, from big companies Google and Apple respectfully, they chose to keep switch/case and just fix the shortcomings. IMO, that should have been the design goal from the start. Don't get me wrong: There are a lot of cool ideas in this proposal. But as a newcomer looking at the todo example, I found myself scratching my head.

Unless this proposal can be a drop in replacement for switch, it's a non-starter. Instead of focusing on being different, the goal should be to simply fix switch and build on it.

If I were to start all over, this is how I would start:

const res = await fetch(jsonService)
match (res.status) {
  case 200:
    const {'Content-Length': s}} = res.headers
    console.log(`size is ${s}`)
  case 404:
    console.log('JSON not found')
  default:
    if (status >= 400) {
        throw new RequestError(res)
    }
}

This is familiar. This is easy to teach. You can say "just use match". Unlike switch It breaks for you and is block scoped. No fall-through allowed. You don't have to dig that deep to understand. And to avoid using switch, you can have your linter remind you. It's auto-fixable. Just replace the keyword "switch" with "match".

Then I would think about expanding a bit by allowing this:

const res = await fetch(jsonService)
match (res.status) {
  case 200:
    const {'Content-Length': s}} = res.headers
    console.log(`size is ${s}`)
  case 404:
    console.log('JSON not found')
  case >= 400:
    throw new RequestError(res)
}

Here case can represent a placeholder for the value, so you can do things like case instanceof Foo.

Then I would build on it like this:

const getLength = vector => {
  match (vector) {
    shape { x, y, z }:
      return Math.sqrt(x ** 2 + y ** 2 + z ** 2)
    shape { x, y }:
      return Math.sqrt(x ** 2 + y ** 2)
    default:
      return vector.length
  }
}
getLength({x: 1, y: 2, z: 3}) // 3.74165

shape is very much like when in the proposal. With the getLength example it would be identical. However, it would not allow when {type: 'set-visibility-filter'} like in the TODO example (in README). Anything that isn't valid in the destructing syntax would not be valid here.

IMO, when {type: 'set-visibility-filter'} is too confusing. And begs the question if when {type: SET_VISIBILITY_FILTER} is the same or not. And if it is if SET_VISIBILITY_FILTER is declared, but isn't when SET_VISIBILITY_FILTER is removed, it is too brittle.

ljharb commented 5 years ago

@lukescott to clarify; i'm not a champion of the proposal, and many different TC39 members will have many different opinions. Mine is that switch is abomination and I want it to be easily distinguishable from this much, much better replacement. Syntax familiarity is only good when it's actually a similar thing - when it's different, familiarity causes more problems in my experience, because people make incorrect assumptions about how something works.

lukescott commented 5 years ago

@ljharb I get that. But sometimes strong opinions can be swaying, and there needs to be a balance. From my perspective, there really isn't anything wrong with switch, if it were "break by default" and block scoped, just like it is in Go and Swift. Adding to a language is really about solving the biggest pain points. Forgetting a break is a huge one, and I will cheer from the mountain tops when that is solved. However, in solving such problems I favor simplicity and familiarity with the least impact.

The idea behind this proposal has merit. But if it doesn't take a step back and focus on simply solving switch's biggest pain point first, it may suffer the same way decorator has.

If this were a brand new language, I would follow Go and Swift's lead on fixing switch. But the language exists, so the next best thing is to take switch, fix it, and use a different keyword. Then build on that.

I'm not saying the way I presented it is the only way. You could take the example I wrote above and do this instead:

const res = await fetch(jsonService)
switch (res.status) {
  when 200:
    const {'Content-Length': s}} = res.headers
    console.log(`size is ${s}`)
  when 404:
    console.log('JSON not found')
  // you can also use "shape" here as well
  when >= 400:
    throw new RequestError(res)
}

Both of these take the existing semantics and change the keyword. With the above, instead of banning the keyword switch in your linter you could ban the keyword case. I kind of like the way match shape and match case reads though!

kaizhu256 commented 5 years ago

@ljharb, why is switch-statement an abomination?

ljharb commented 5 years ago

@kaizhu256 unintentional fallthrough is too easy, cases don’t have a scope unless you remember to add curly braces, the only matching criterion is ===, to name a few. Needless to say, this repo isn’t the place to discuss that - let it suffice that i consider it so. Feel free to find me on irc if you have further questions.

FireyFly commented 5 years ago

As far as I understand it, this isn't a proposal for an improved switch statement per se. The proposal is for a pattern matching construct, as widely found in functional languages--hence, proposal-pattern-matching. Getting rid of the pattern matching and pushing for a switch-with-autobreak construct seems like a very different proposal, so it doesn't feel too constructive to discuss it here.

lukescott commented 5 years ago

And yet they are very closely related. In Scala, for example match is a first-class feature, yet @switch is an annotation for match. Some examples:

// match without annotation. Looks like a switch, doesn't it?
val month = i match {
    case 1  => "January"
    case 2  => "February"
    case 3  => "March"
    case 4  => "April"
    case 5  => "May"
    case 6  => "June"
    case 7  => "July"
    case 8  => "August"
    case 9  => "September"
    case 10 => "October"
    case 11 => "November"
    case 12 => "December"
    case _  => "Invalid month"  // the default, catch-all
}
// with annotation
class SwitchDemo {
    val i = 1
    val x = (i: @switch) match {
        case 1  => "One"
        case 2  => "Two"
        case _  => "Other"
    }
}

(Examples from https://alvinalexander.com/scala/how-to-use-scala-match-expression-like-switch-case-statement)

So Scala, which is functional language, replaced match with switch. Does that seem fair to say? So if you are building a feature inspired from a functional language that replaced switch with match, how can you say they are different or the discussion isn't constructive?

IMO, you really can't discuss one without the other. And when discussing features to add to a language, you really have to be conservative and ask yourself what are you trying to solve. A switch with a missing break is a huge pain point, something worthy of fixing. Pattern matching is a nice to have, not a pain point. I'm not saying you can't have both. The priorities are just backwards.

And the end of the day, you can't design a feature in isolation. You have to consider how well this pairs with the rest of the language and the users who write it. I don't write Scala, I write JavaScript. It has to make more sense to people who write JavaScript, not Scala (or <insert other functional language here>).

FireyFly commented 5 years ago

I feel like I might've come off as more dismissive than I meant to be, sorry...

It's true that you can use a pattern-matching expression to implement something akin to a switch (although I note that the example you gave does alleviate what I would consider to be the biggest annoyance with switch: not being able to use it in expression contexts).

I certainly agree that it makes sense to consider the rest of a language--its features, how developers use the language in practice, etc--when adding a new feature. I suspect that is why this repo's README has examples from some common JS libraries/contexts, discusses how the proposed pattern matching relates to the destructuring bindings that are in the language today, etc. It's also true that a pattern-matching construct is embracing the functional side of JS more--certainly when compared to switch--though the same goes for many other recent JS features as well.

I'm not necessarily saying one proposal is more important than the other, but if you want to propose a shorthand for adding break to all branches of a switch, I'm sure such a proposal would be welcome too. FWIW, I don't think I'd consider the auto-break a major pain point (even though I'd prefer if switch had been designed that way to start with, like some newer languages do)--but, whilst that could be a fruitful discussion, I'm not sure this is the repository for it.

Edit: just to be as unambiguous as possible: I don't necessarily think all comparisons with switch statements are inherently bad here--it could be plenty interesting to see especially what some more recent languages have done with their switch statements (like Swift, Rust, Go). I just think scaling away everything from the proposal seems like a different proposal altogether, and that the pattern matching is at the core of the, well, pattern-matching proposal.

zkat commented 5 years ago

I chose -> because after putting nontrivial examples side-by-side, using : and other options, -> provided the best visibility by far, in practice. I'm concerned that : will make it much harder to see the end of a match when using Object destructuring syntax, since that itself uses :. This was my experience when sketching out various examples.

texastoland commented 5 years ago

I only skimmed the thread. I'd like to point out that -> in languages like Haskell and Ocaml and => in Scala explicitly denote that pattern matching is precisely a composition of partial functions. Inventing a new type of arrow for pattern matching seems conflated. The ideal would be to reuse bound arrow syntax and pattern match on function arguments the same way as patterns.

zkat commented 5 years ago

I'm concerned that using => will create the wrong expectations about the binding and scoping semantics.