tc39 / proposal-pattern-matching

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

C# 8.x have interesting syntax too #146

Closed Akxe closed 4 years ago

Akxe commented 4 years ago

The current switch proposal differs too greatly feel like part of JS. We should try to reuse parts of JS switch if we want this to be accepted. Maybe we could take a look at syntax that C# uses. Lately, C# has been taking a lot of notes from JS/TS world, thus it might make more sense to look into its syntax for JS.

Syntax example:

Simple (edited to fit JS world)

Car, Truck, Train and Metro are user-defined classes.

(vehicle) switch {
  case Car:
    console.log('This is a car!');
    break;

  case Truck:
    console.log('This is a truck!');
    break;

  case Train:
  case Metro:
    console.log('This is a transport!');
    break;
}

Multiple values

If the expression passed is inline array, one can use deconstruction-like syntax to do the matching.

([vehicle, person.ageGroup]) switch {
  case [Bus, ageGroup.child]:
  case [Bus, ageGroup.eldery]: 
    console.log('Make it free');
    break;

  case [Bus]: 
    console.log('They can pay!');
    break;

  case [Car]: 
    console.log('These polute!');
    break;

  case [, ageGroup.alien]: 
    console.log("Interesting, isn't it?");
    break;

  default:
    console.log('This is something...');
    break;
}

if once wants to use this syntax on array in variable: ([...array]) switch { ... }.

Pattern matching

(vehicle) switch {
  case of { wheels: 2 }:
    console.log('This is a car!');
    break;

  case of { wheels: 4 }:
    console.log('This is a car!');
    break;
  case of { }:
    console.log('This is matches any object!');
    break;
  default:
    //  This would match null and similar in this case.
}

Points against current syntax:

JS doesn't need nor want more keyword if necessary.

ljharb commented 4 years ago

If it reuses parts of switch, it will not advance. It must be entirely new for me to agree to advancement - for learnability, googleability, confusion, etc.

noppa commented 4 years ago

Note that your first example pretty much works already with the existing switch-cases with a minor modification (apart from possible issues with inheritance or realms).

let vehicle = new Truck()

switch (vehicle.constructor) {
  case Car:
    console.log('This is a car!');
    break;

  case Truck:
    console.log('This is a truck!');
    break;

  case Train:
  case Metro:
    console.log('This is a transport!');
    break;
}

// This is a truck!
Akxe commented 4 years ago

Yes @noppa, unless you extend classes.

ljharb commented 4 years ago

instanceof is forgeable and does not work across realms; i would also not be interested in enshrining instanceof comparisons into pattern matching (except when a user chooses explicitly to use it, ofc).

(to be clear, I’m not a champion of this proposal; just a TC39 delegate)

Akxe commented 4 years ago

@ljharb Nor does .constructor as you cannot have the actual reference to it. But it wasn't really the point. The point is the same as many others here raised. This syntax is like one from completely different language, that is based around the different syntax. It is very alien to me and based on number of issues on syntax theme, I don't think I am the only one...

I wanted to propose different syntax that is based on language with similar syntax. The author of this proposal is currently writing in rust and thus the syntax is from rust, with exception of the arrow, that has different width, but from what I read she wanted the fat one too. I don't think it is a good idea to diverge from JS syntax this much.

PetterSa commented 4 years ago

@Akxe In my opinion, and several others, defaulting to fallthrough is a defect and we should absolutely take this as an opportunity to move away from it and the break keyword.

See for example https://stackoverflow.com/questions/252489/why-was-the-switch-statement-designed-to-need-a-break

The most relevant bit is this by stack-overflow user Michael Burr:

Many answers seem to focus on the ability to fall through as the reason for requiring the break statement.

I believe it was simply a mistake, due largely because when C was designed there was not nearly as much experience with how these constructs would be used.

Peter Van der Linden makes the case in his book "Expert C Programming":

We analyzed the Sun C compiler sources to see how often the default fall through was used. The Sun ANSI C compiler front end has 244 switch statements, each of which has an average of seven cases. Fall through occurs in just 3% of all these cases.

In other words, the normal switch behavior is wrong 97% of the time. It's not just in a compiler - on the contrary, where fall through was used in this analysis it was often for situations that occur more frequently in a compiler than in other software, for instance, when compiling operators that can have either one or two operands:

switch (operator->num_of_operands) {
    case 2: process_operand( operator->operand_2);
              /* FALLTHRU */

    case 1: process_operand( operator->operand_1);
    break;
}

Case fall through is so widely recognized as a defect that there's even a special comment convention, shown above, that tells lint "this is really one of those 3% of cases where fall through was desired."

I think it was a good idea for C# to require an explicit jump statement at the end of each case block (while still allowing multiple case labels to be stacked - as long as there's only a single block of statements). In C# you can still have one case fall through to another - you just have to make the fall thru explicit by jumping to the next case using a goto.

It's too bad Java didn't take the opportunity to break from the C semantics.

In addition I am also not in agreement with the core statement of your argument that we need to reuse existing parts of JS. I feel that if the syntax is too similar but with very different functionality then it will be harder to differentiate between them and easier to mix them up by mistake. They should be very different to show that they are not the same functionality.

Akxe commented 4 years ago

To me, fallthrough is a key feature of the switch. That being said, if there is a block that is defined using { ... } syntax as block usually is, I would assume that there would be no fallthrough and be ok with it but as long as there isn't a block. I would see it as a bug.

I met JS very early, and I have stayed on it for the past 10 years. I tried (usually because of school) PHP, C#, Java, Ruby, Python and possibly some I forgot already. I never felt any of these languages to be superior to JS; I just wanted some small extract of their syntax.

@PetterSa if you use syntax that is completely alien to JS this would feel as some hack that JS has had due to its evolution (the fact that JS ignores <!--, and similar quirks). The language syntax needs to stay consistent; I am sure you agree with it.

The core of this proposal to be able to switch branches based on condition, this is the thing switch was created for, thus it should be used to do so.

switch (*expression*) {
  case 'value1': // Falltrought
  case 'value2' {
    ... no need of `break`, as this is a block
  }
  case of { error } {
    ... this expression has an error in it ...
  }
}

can be the perfectly valid syntax, it feels similar enough to be sure what to expect. (If this would be the one to go with I would split this proposal to two for greater chances of success.)

FireyFly commented 4 years ago

It seems very dangerous to have the presence of a colon affect whether or not there's an implicit break after the case...

switch (expr) {
  case 'value1': {
  } // fallthrough
  case 'value2' {
  } // no fallthrough
}

To me, fallthrough is a key feature of the switch.

But it doesn't really relate much to pattern-matching. This is probably one of the resaons why there's a strong desire to not mix in syntaxes from a related-but-different syntactic construct, since the semantics (or at least many of the intended uses, borrowing from functional languages) is different in many ways.

Like, (to me) this isn't "let's make switch but with some additional nice features", but more "let's borrow pattern-matching from functional languages because it's a powerful construct that would fit in quite well with the way JS is often used these days". The difference in viewpoint here is probably what leads to these discussions of "should the syntax be similar to switch or not".

Akxe commented 4 years ago

The difference I proposed is that you have to choose. If case expression is followed by : it fallthrough unless break is used. But if it is fallowed by block{ ... }. It will not fallthrough anymore. This will enable you to do things like:

switch (enumValue) {
  case enum.option1 {
    console.log('only run for option 1!');
  }

  case enum.option2:
    console.log('this runs only for option2.');
  case enum.option3:
  case enum.option4 {
    // This would be a lexical block, where `this` is inferred from the parent block
    let variable = 5;
    console.log('only run for option 2, 3 or 4!');
  }
}

console.log(typeof variable); // undefined in all cases

This is one way to solve fallthrough. It has distinct syntax yet similar to JS eyes while conveying the message of the block better that will run better.

Note: I don't necessarily like the fact that option2 does what it does, but it is used and it has its uses.

Then there is the second part of the pattern matching:

switch (*expression*) {
  case of { error } {
    //  ... this expression has an error in it ...
  }
  case of { message } do {
    //  The syntax with do might be better on eye
  }
}
FireyFly commented 4 years ago

@Akxe it is already legal today to follow a case with a block, which is why I made that remark about the existence of a colon implicitly adding a break after the block. My value1 case in my earlier example is already legal syntax today, with well-defined behaviour (that of course can't change).

Akxe commented 4 years ago

@FireyFly Oh, I didn't think of that... let's then use the do keyword for all non-fallthrough blocks.

I now see what you mean... Although they seem unrelated. I do not think they should be completely separated. To me, pattern matching should be part of switch syntax, but it should build upon new switch non-fallthrough syntax.

Having completely new syntax for pattern matching is quite too big to be added to JS. A function would is much more appropriate.

PetterSa commented 4 years ago

@Akxe Per the link i referenced it is clearly explained why fallthrough is considered a bad feature for most people, I will say the arguments presented there still holds and I can't really see a good counter-argument in your posts. I have also updated my post with the relevant answer from stack-overflow.

This proposal suggests not only to expand with enhanced abilities to switch branches, but also to return a value based on what was selected. This makes it not only a feature for switching branches, but also something more.

In my eyes there is enough to justify a new syntax:

  1. Current switch syntax is not optimal
    1. Fallthrough by default
    2. Not block-scoped by default
  2. New features that makes switch not sufficient to describe the functionality
Akxe commented 4 years ago

@PetterSa yes but only if one does not include this as fallthrough:

switch (over) {
  case 'value1': 
  case 'value2': 
    ... do something...
}
  1. Falltrought could be made optional very easily for example case expression do {}.
  2. This is a non-breaking change worth adding to switch.

I don't really see why to add new syntax and reserve new keywords for something that fits so nicely in existing while making the current switch better.

ljharb commented 4 years ago

@Akxe because imo switch is terrible and should never be used, and I don't want to suddenly have to start advocating "use switch but only if you use it with this exact syntax", because that is confusing for newcomers and veterans alike. Pattern matching is a good thing I want to advocate everyone use over switch, and that requires it be distinct syntax.

PetterSa commented 4 years ago

@Akxe Pattern matching also supports returning a value based on what was chosen, how do you suggest implementing this without breaking existing switch statements?

Akxe commented 4 years ago

@PetterSa how about the do expression

Akxe commented 4 years ago

imo switch is terrible and should never be used

@ljharb Nothing is this one-sided and if you think it is then you might not have understood them properly...

The issue of syntax will never get resolved like this, thus I'll be closing this.

PetterSa commented 4 years ago

@Akxe Are you suggesting that some of the cases should return something and some of them not? That sounds like a nightmare of bugs. Also, the switch statement does not return values, introducing that would mean breaking backwards compatibility.

Akxe commented 4 years ago

@PetterSa No. Like this

const result = do {
  switch (n) {
    case 1:
      'first';
      break;
    case 2 do {
      let a = 'second'
      a;
    }
    default: 
      n;
  }
}

Switch itself doesn't need the functionality as wrapping the whole switch in do {} expression block will suffice.