Proposal-pattern-matching: New syntax (probably again)

Created on 18 Mar 2019  路  26Comments  路  Source: tc39/proposal-pattern-matching

Example of current proposal:

const getLength = vector => {
  case (vector) {
    when { x, y, z } ->
      return Math.sqrt(x ** 2 + y ** 2 + z ** 2)
    when { x, y } ->
      return Math.sqrt(x ** 2 + y ** 2)
    when [...etc] ->
      return vector.length
  }
}

New proposed syntax:

const getLength = vector => 
  do (vector) {
    ({ x, y, z }) -> Math.sqrt(x ** 2 + y ** 2 + z ** 2),
    ({ x, y }) -> Math.sqrt(x ** 2 + y ** 2),
    () -> vector.length, // or _ ->
  }

Motivation:

  1. Avoid switch/case keywords and "when".
  2. Maximal reusing already existing compiler's parsing job
  3. Use expression instead statement for matching which allow eliminate repeating return for each case and also allow reusing matched result later.
  4. do already reserved keyword. Existing parser expect do {, so then it come across to do ( it could try start parsing this differently. Also matching have many similarities with single iterated Dijkstra's Guarded loop and relate to do expressions proposal

Thoughts?

PS More examples:

const shallowClone = x =>
  do (x?.constructor) {
    String -> x + '',
    Array  -> x.slice(),
    Object -> ({ ...x }),
    Date   -> new Date(x.getTime()),
    _      -> x,
  }
const fib = n =>
   do (n) {
      0    -> throw new RangeError('zero is not possible'),
      1, 2 -> 1,
      3    -> 2,
      _    -> fib(n - 1) + fib(n - 2),
   }
const fizzbuzz = n => { 
  console.log(
    do (n % 3, n % 5) {
      (0, 0) -> 'FizzBuzz',
      (0, _) -> 'Fizz',
      (_, 0) -> 'Buzz',
      (_, _) -> n,
    }
  )
}

worst examples with existing proposal:

switch (y) {
  case 0: case 1: case (x) {
    when 1 -> { z = x }
  }
  break
}

switch (y1) {
  case 0: case (x1) {
    when 1 -> 
      return x1
    when 2 -> 
      return y1
  }
}

same examples with new syntax:

switch (y) {
  case 0: case 1: do (x) {
     1 -> { z = x },
  }
  break
}

switch (y) {
  case 0: return do (x1) {
     1 -> x1,
     2 -> y1,
  }
}

Using as IIFE

const result = (do (type) {
  'cdf' -> (x, mean, std) => 0.5 * erfc(-(x - mean) / (std * Math.sqrt(2))),
  'pdf' -> (x, mean, std) => (
    Math.exp(-Math.pow(x - mean, 2) / 2)) / (std * Math.sqrt(2 * Math.PI)
  ),
  _ -> () => {},
})(0.5, 0, 0.33)

Most helpful comment

I imagine replacing case with do would be similarly problematic if/when do expressions happen--and overload the do keyword further with different meanings.

All 26 comments

There's already #116 about how to meet desires for both blocks and expressions as case bodies (well, specifically for how to re-introduce the expression case).

Making the cases syntactically identical to arrow functions might be a bit confusing since you wouldn't be able to extract the case out into a function and refer to it by identifier. I kinda like the parameter-list-style syntax for the pattern though..

Yeah, arrow syntax could be conflict with declared functions inside case body, so I switching to ->

In current proposal case is a statement since pattern matching not necessarily should return a value. You proposal changes pattern matching semantics by replacing statement with expression.

yes, I mentioned about replacing statement with expression. I don't see any problems with that. Semantics with expression in JavaScript don't require explicitly return some value and just automatically return undefined

I'll elaborate.

I don't see how your proposal helps in

Maximal reusing already existing compiler's parsing job

In fact parser doesn't know syntax from your proposal at all. Also it interferes with do expressions proposal.

Use expression instead statement for matching which allow eliminate repeating return for each case and also allow reusing matched result later.

Omitting return is valid in do expression (JSX example from motivating examples).
Is it possible to have block after -> in your proposal? Current proposal lets you do any side effects. If so, should do expression propagate to blocks in right hand side and return result of latest expression in block or should block in RHS return undefined?

In fact parser doesn't know syntax from your proposal at all. Also it interferes with do expressions proposal.

Look at this part:

.. {
  ({ x, y, z }) -> Math.sqrt(x ** 2 + y ** 2 + z ** 2),
}

It pretty the same as:

{
  '0': ({ x, y, z }) => Math.sqrt(x ** 2 + y ** 2 + z ** 2),
}

Which already valid and parsed. Of course do (expr) { require additional effort.
Of course this tokenized as DFG entity, not as object.

Is it possible to have block after -> in your proposal

Yes. Semantics in -> the same as arrow function.

Also

const getLength = vector => 
  do (vector) {
    ({ x, y, z }) -> Math.sqrt(x ** 2 + y ** 2 + z ** 2), // comma operator?
    ({ x, y }) -> Math.sqrt(x ** 2 + y ** 2),
    () -> vector.length,
  }

comma is operator the same as when just put in the end instead front as in case with when

Could ; be used instead to remove ambiguity?

yes, I think ; could be used instead ,. But ; usually optional in js. so I think , still little better for this case

Yes, it's the same issue as in https://github.com/tc39/proposal-class-fields/issues/7#issuecomment-309879686. Still, using ; is much better than , overloading inside do body (and only in case when do has (expression) before its body).

The comma is operator and can be inside expressions. The semicolon is not an operator and cannot be used inside an expression. It is used as part of JavaScript syntax to mark the end of an expression that is being treated as a statement and using as terminator instead \n. Of course current in current case we use block which is set of statements and could use commas or semicolons.

I'm inclined to do this the way #116 proposes tbh.

@zkat but what about switching from case to do? See this example:

let a = ...
switch (b) {
    case (a): case (a) {
       when 1 -> ...
       whrn 2 -> ...
    }
}

case (a) inside switch block also valid and could cause to parsing problem (or will require back tracking) and human confusion

Of course current in current case we use block which is set of statements and could use commas or semicolons

I still don't agree with that because semantically comma operator has completely another meaning than semicolon and comma usage in your examples is error prone and unintuitive IMO.

I also prefer #116 though example from @MaxGraey example is ambiguous indeed

@chicoxyzzy @MaxGraey case is not ambiguous from a technical perspective, and it's relatively easy to disambiguate: just look for a : or a { after parsing parenthesized case heads. Edit: And it doesn't require backtracking, either. It only requires a single token of lookahead.

  • case ( expr ) - In both cases, expr is a RHS expression, so it's parsed identically. This stands in contrast to async arrow functions vs function calls, since async ( { foo = 1 } ) ; is not valid, but async ( { foo = 1 } ) => expr is.
  • case ( expr ) : - A case label is of this form.
  • case ( expr ) { - A case expression starts with this form.
  • case expr : - Also a case label.
  • case expr { - Syntactically invalid - case expressions' conditions must be enclosed in parentheses.

(If it were ambiguous, it would've been caught by now and nobody would still be pushing it forward.)

Sorry, by ambiguity I mean that it's not readable enough when combined with case clause from switch statement as in example above. It's not parsable by humans nor machines, so probably I chose the wrong word 馃檪

I did understand the "visually ambiguous" bit - I was just responding to the "technically ambiguous" claim by showing it was not.

Visually, I'm not convinced it's as ambiguous as you're making it seem, simply because it lacks that colon. Missing the colon before anything other than an open curly brace is already enough to get runtimes to yell at you if you're not already using a linter shouting even louder, so I find it unlikely anyone is going to forget that. A linter could warn about using a case statement within a switch/case since it's uncommon and potentially confusing, but this lies in a similar area as bitwise operators vs logical operators IMHO - if you're used to the syntax and precedence rules and aren't afraid to mix them both, you'll know it's different (and be less likely to make the mistake), but if you're not, you may benefit from such a lint rule.

I imagine replacing case with do would be similarly problematic if/when do expressions happen--and overload the do keyword further with different meanings.

@FireyFly How this could conflict with do expressions proposal? do expressions expect do { during parsing, but this proposal provide do ( which could determine earlier even case (x): vs case (x) {.

@MaxGraey I meant that it seems equally prone to confusion for humans as with case, and also that it would mean the do keyword has very different meaning in different contexts (do-while loops, do expressions, do pattern-matching). They seem equally (un)problematic from a formal parsing point-of-view, so I'm not concerned about that.

Unfortunately, the "case" also does not greatly correct the situation. Ideally, it would use the "match" keyword, but reserving this new keyword would lead to far more non-semantics conflicts.

I don't think it isn't even worth talking about reserving a new keyword.

Between this and #116, I found this syntax cleaner and would honestly use it without problems. The motivations are clear and the fact that do ( can easily be caught during parsing is great.

@tloriato It's easy, but that's not quite where you would draw the distinction. It would work more like case-within-switch, requiring a nearly identical disambiguation step to separate it from do ... while. The only three differences are in the initial keyword (do vs case), the specific token itself used to branch with ({ vs while), and where it needs to be checked (all statement expressions vs only in the immediate block of switch/case bodies). Performance considerations would also remain virtually identical.

  • do ( expr ) { - A pattern-matching do expression, where expr is the condition.
  • do ( expr ) ; - A do ... while statement, where ( expr ) is the statement body.
  • do ( expr ) <LineTerminator> while - A do ... while statement, where ( expr ) is the statement body.
  • do expr { - Invalid.
  • do expr ; - A do ... while statement, where ( expr ) is the statement body.
  • do expr <LineTerminator> while - A do ... while statement, where ( expr ) is the statement body.

I'm not willing to conflict with do expressions (as https://github.com/tc39/proposal-pattern-matching/issues/136#issuecomment-477535349 pointed out)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

samuelgruetter picture samuelgruetter  路  3Comments

arcanis picture arcanis  路  6Comments

xtuc picture xtuc  路  4Comments

JLHwung picture JLHwung  路  6Comments

caub picture caub  路  8Comments