Proposal-pattern-matching: Simplify Syntax

Created on 25 Nov 2019  路  40Comments  路  Source: tc39/proposal-pattern-matching

the current proposed syntax involves tokens such as case and when.

Couldn't things be simplified as the following?

const fn = (vector) -> 
  | ({ x, y, z }) => x + y + z;
  | () => new Error('vector cannot be empty');

fn({ x: 2, y: 2, z: 2 }); // 6
fn(); // Error('vector cannot be empty')

you would basically be able to define different functions under the same name.

  1. -> identifies the pattern matching
  2. | identifies a case
  3. Cases are written in the form of a function...

Similar to what you can do with haskell

syntax discussion

Most helpful comment

I would prefer to drop using case and instead use the more expressive name match, stop prefixing each of the matchers with when to be more concise and this frees up when (instead of if in the proposal to avoid keyword overloading) to be used for MatchGuard

const response = {
    status: 200,
    body: {
        id: 5,
        value: 6
    }
}

// or just return match (...) ...
const data = match (response) {
    // Basic matching while making it less verbose. 
    { status: 200, body } -> body.id;

    // Pattern matching with an expression working as a predicate
    { status, body: { error } } when (status >= 500) -> error;
    { status } when (status >= 400 && status < 500) -> 'Authorization error';

    // Multi-expression body can still use curly braces
    // However it will need a keyword, returning the last expression of a body
    // is not suitable for JS. 
    { body } -> {
        console.log('Just returning the body, might need a keyword');
        body;
    }
    // if nothing matches then data will be undefined
}

As a side note: pin operator would be good idea, saw that it's in the TO_INFINITY_AND_BEYOND part, but I think it's valuable enough to be included in the initial spec

All 40 comments

It sounds like this syntax would restrict pattern-matching to arrow function constructs. I think having pattern-matching be a general-purpose expression and not restricted to defining functions is important for a lot of use-cases, so I'm not sure that's a good restriction to make. I'm also not sure why the use of -> and => tokens is this way around, considering => already indicates a function as it is. If anything I'd expect those to be swapped.

Not sure what the problem is with using keywords such as case and when? In which regard are we simplifying the syntax here? I'd also note that Haskell uses case for its pattern-matching construct as well, see e.g. https://en.wikibooks.org/wiki/Haskell/Control_structures#case_expressions

@FireyFly I do not agree with your arguments. I think its just another perspective of this concept in general.

the following example indicates case as a function-call and when "pattern" -> as mysterious pattern-handler.

const str = whatDoYouWantToSay()
case(str) => {
  when "hello" -> sayHello(),
  when "bye" -> sayBye(),
}

The next example on the other hand:

const res = getSomeResult()
(res) -> 
  | "hello" => sayHello(),
  | "Bye" => sayBye();

...does not provide a case-function call. Instead (var) -> is an indicator, that the following sequence of | (pattern) => expression, | (pattern2) => expression; should compile to local functions of the form:

handleX = (pattern) => expression;
handleX = (pattern2) => expression;
handleX(getX())

This means, that this way pattern matching, could be realized on the function compilation level, and case would be some sort of a helper, for local produced values.

But with the function-level concept, you are more flexible, lets imagine, we could define pattern-matching functions ourself

handleError = (404) => "Not found"
handleError = (401) => "Not Authorized"
export handleError

// import handleError and use it..
getUser().then(handleError)

OR in case of case, it even gets more interesting, because we can compose different pattern-matched functions, like so:

(result) ->
  | handleSuccess,
  | handleError;

So, the function level concept is just more flexible, and makes it possible to build on top of it.
(who finds grammar typos, can keep them)

Even pm proposal for C++ has less verbose syntax:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1371r1.pdf

inspect (s) {
  "foo": std::cout << "got foo";
  "bar": std::cout << "got bar";
  __:    std::cout << "don't care";
}

I would prefer to drop using case and instead use the more expressive name match, stop prefixing each of the matchers with when to be more concise and this frees up when (instead of if in the proposal to avoid keyword overloading) to be used for MatchGuard

const response = {
    status: 200,
    body: {
        id: 5,
        value: 6
    }
}

// or just return match (...) ...
const data = match (response) {
    // Basic matching while making it less verbose. 
    { status: 200, body } -> body.id;

    // Pattern matching with an expression working as a predicate
    { status, body: { error } } when (status >= 500) -> error;
    { status } when (status >= 400 && status < 500) -> 'Authorization error';

    // Multi-expression body can still use curly braces
    // However it will need a keyword, returning the last expression of a body
    // is not suitable for JS. 
    { body } -> {
        console.log('Just returning the body, might need a keyword');
        body;
    }
    // if nothing matches then data will be undefined
}

As a side note: pin operator would be good idea, saw that it's in the TO_INFINITY_AND_BEYOND part, but I think it's valuable enough to be included in the initial spec

or simply extend the usage of the case token...

const bar = (n) => n === 1 ? 'one' : 'others';

would become

case bar = (1) => 'one';
case bar = (n) => 'others'
bar(11); // 'others'
bar(1); // 'one'

Personally, I don't mind when. I might prefer something like where if the language is going to use a word as the keyword. But to use our already existing case involved in switch statements seems like it would be confusing to people who have not yet been exposed to pattern matching. Part of the benefit of using keywords like match and when is that it's clearer for people new to pattern matching. I think especially of new programmers who have never experienced any programming language other than ECMAScript.

While I personally love the terseness of the OP's example, and it takes from other languages like Haskell, Elm, etc., I contend that match would be significantly easier to pick up, and still takes from the pattern (pun intended) of some other languages, including Rust.

Remember JS have complex syntax.

@hitmands

const fn = (vector) -> 
  | ({ x, y, z }) => x + y + z;
  | () => new Error('vector cannot be empty');

Consider those love semicolon-less coding style:

const fn = (vector) -> 
  | ({ x, y, z }) => x + y + z
  | () => new Error('vector cannot be empty')

then become:

const fn = (vector) -> 
  | ({ x, y, z }) => x + y + z | () => new Error('vector cannot be empty')

Second | would be interpreted as bitwise OR, and then an arrow function.


@KristjanTammekivi

match is not reserved word in JS, so the syntax match (x) { would require no line terminator before {, or it will become a match() method call followed by a block (and syntax error when meet ->). case (x) { do not have such problem because case is a reserved word.

Hi @hax ,

I see how this would introduce error proneness,
but the overall idea of having pattern matching implemented via functions is still powerful imho...

On top of what said here

Id also add that:

case error = (401) => 'Unauthorized';
case error = (400) => 'Bad Request';

It is very clear since it doesn't need to:

  1. introduce any new keyword / glossary / syntax
  2. people are already used to case in swtich-case and its behaviour would be mostly preserved
  3. it is very familiar to overloading

even if the currently proposal would go ahead,
I think we should still provide a kind of alias based on this approach...
As you can do in Haskell

@hitmands

I don't sure what the semantics of your example. Does it create a function named error? What will it return if not match? Could you provide a complete example?

I guess it's how done pattern matching in picat:

merge([],Ys) = Ys.
merge(Xs,[]) = Xs.
merge([X|Xs],Ys@[Y|_]) = [X|Zs], X<Y => Zs=merge(Xs,Ys).
merge(Xs,[Y|Ys]) = [Y|merge(Xs,Ys)].

and Haskell:

map _ []     = []
map f (x:xs) = f x : map f xs

Hi @hax ,

exactly... We'd allow for a top-level case that behaves like const
with the possibility of overloading the previous declaration.

case error = (401) => 'Unauthorized';
case error = (400) => 'Bad Request';

error(500); // undefined

That seems exceedingly complex. What if you add error to a Set after each one, what鈥檚 the final size of the set? What鈥檚 the function toString of error?

I think case is a good one to start with.. It's explicit, and clear and not reserved outside the switch context, afaik.

But, if the case is not explicitly defined like in your example error(500); It would be better to throw an exception, instead of undefined, imho. Because FP is about declarative code.

case error = (401) => 'Unauthorized';
case error = (400) => 'Bad Request';
case error = (_) => "Server Error"

// pseudo type code (imagine TS or Flow..)
type error = (int) => string

(Altho I鈥檝e said this before; i strongly object to any overlap whatsoever with switch; case imo is not an option)

@ljharb I guess that most of us do not have the deep knowledge required to understand this constraints. We are just looking at it from the users perspective (DX) and trying to come up with an idea.
We've to understand the constraints, which we have in JS, to come up with a proper suggestion.

This ones not about deep knowledge :-) i feel strongly that any overlap with switch will hurt DX by creating confusion and harming googleability.

i feel strongly that any overlap with switch will hurt DX by creating confusion and harming googleability.

Ok, so, it's all about naming :)

So, it would be possible to make normal functions work?

const moneyToString = (100, "USD") => "Hundert bucks"
const moneyToString = (1, "USD") => "1 dollar"
const moneyToString = (n, "USD") => `${n} dollars`
const moneyToString = (n, _) => moneyToString(n, "USD")

@ljharb can you please dive a bit more into:

That seems exceedingly complex

case is already a reserved word in javascript, and cannot be used outside of a switch block.
This would only allow for it to be a top-level expression.

I see it very much like the await keyword, which could only be used inside async functions until https://github.com/tc39/proposal-top-level-await

I don't see how people could be confused with the switch statement since there is no mention of switch statements whatsoever...


What if you add error to a Set after each one, what鈥檚 the final size of the set?

this is TBD, however... since we are only talking about overloading, I assume the size of the set would not change... but this is more of an open conversation.

What鈥檚 the function toString of error?

This is a TBD again, I honestly don't know... I'd expect it do behave as functions do, with the only addition of surfacing overloading.

case error = (401) => 'Unauthorized';
error.toString(); // `(401) => 'Unauthorized'`

case error = (500) => 'Internal Server Error';
error.toString(); // `(401) => 'Unauthorized'\n(500) => 'Internal ServerError'`

but again, this is really TBD

If we really want support some form of pattern-match based function overloading, I supposed it should not allow inserting other statements. At least not in the beginning.

It would be better to throw an exception, instead of undefined

@webdeb I agree! Return undefined seems confusing, because a matched function could also return undefined.

would be possible to make normal functions work?

I think the DX problem may be it's unusual for most JS programmers who don't have the knowledge of haskell (or similar languages), and it also very different to other mainstream languages overload (which is dispatched statically based on type info of signature).

Even in FP land, it seems some languages (ML family like OCaml, F#, ...) do not use this form?

it's all about naming :)

I'm neutral on naming in this specific issue. If case funcName is confusing, we could consider case function funcName() {} though it's lengthy.

Anyway , the real problem which bother me everytime I try this proposal (and also some others I know who tried this proposal) , is the confusion of pattern vs expression.

Current destructuring syntax in JS also look close to expression, which confused some programmers in the beginning, but we are accustomed to it very soon. Unfortunately it seems we lose the luck this time.

Here are some thought:

When using destructuring, (in most cases) only simple identifiers are involved, but this proposal introduce literal to the destructuring-like syntax.

At first glance it's ok to interpret literal as equality test what is a good "pattern match", but in real programming you soon meet problems.

For example, in many coding style requirements, you should avoid magic numbers, so we will refactor the magic numbers to constants. But in this proposal, or all variants in the above comments:

// other syntax is irrelevant, so just use keyword for clarification
match (500) return 'Internal Server Error'

when someone search all magic numbers and refactor:

// defined in somewhere
const HTTP_CODE_500 = 500

// other syntax is irrelevant, so just use keyword for clarification
match (HTTP_CODE_500) return 'Internal Server Error'

It actually change the semantic totally, but still runnable. You just always return 'Internal Server Error' now 馃槺

Even the built-in constants also have the problem:

match true return 'TRUE' // ok
match false return 'FALSE' // ok
match null return '' // ok
match undefined throw 'Something is wrong' // wrong, you create var "undefined"

match 1 return 'one' 
match 2 return 'two'
match NaN return 'Not a number!' // wrong, you create a local var named "NaN"

Basically, I feel it's very hard for the programmers to learn/understand/remember some identifiers (keywords) are ok, some are not.

Would experienced programmers could behave better? I mentioned destructuring only involve simple identifiers in most cases, but actually you could also use any LHS in [a.b, c] = .... Now, literals are obviously not LHS, which make the brains of many experienced programmers who know LHS well automatically switch the mode from pattern to expression! After second thought, they may figure out the literal is a pattern instead of expression here, but next time they may still be caught. The longer experiences you have on JS, the more likely you will be repeatedly caught, because the mind-set have been built in your brain many years: literals are expressions.


Pattern match is very useful feature, I want it. But it seems we still need to investigate the syntax, not only keywords and structure, but also pattern syntax.

// defined in somewhere
const HTTP_CODE_500 = 500

// other syntax is irrelevant, so just use keyword for clarification
match (HTTP_CODE_500) return 'Internal Server Error'

Isn't it the same as

const myNumber = 500;

function myFunction(myNumber) {
  // do something
}

const myFunction2 = (myNumber) => /* do something */;

So this may not be a problem 馃 It's just shadowing

I think the problem here is the mixing of the match pattern and accessing the matched value. What about something like this:

const four_x_x = n => 400 <= n < 500
const httpToText = case {
  200: () => "success"
  500: () => "internal server error"
  four_x_x: code => `4xx error: ${code}`
}

const parsedResponse = await case (await fetch(...)) {
  { statusCode: 200 }: res => res.json()
  { ok: false }: res => throw new Error(`Failed: ${res.statusCode}`)
}

A major benefit of pattern matching is the ability to directly bind names in the pattern to values. @j-f1 's solution adds an indirection layer that I find less preferable to the direct name binding. The solution also should demonstrate multiple name bindings.

Alternatively, Rust solves this problem by always shadowing variables and never shadowing constants. Perhaps we can apply a similar solution here.

always shadowing variables and never shadowing constants

Do you mean, it will shadow var and let but not const? It might be more confusing to developers

Another thought:

const x = 1
case (input) {
  when x => expr // by using =>, the left hand side is interpreted as variable compare (not a binding pattern )
  when {x, ...expr} => expr // match {x: 1, ...rest} or syntax error?
  when x -> expr // by using ->, the left hand side is interpreted as binding pattern
  when {x, ...expr} -> expr // match {x, ...rest} (current behavior)
}

@Jack-Works Introduce both => -> for same context but have very different semantics seems not a good idea. I remember one complain about fat/thin arrow func in coffeeescript was it's hard to recognize/differentiate them and cause bugs.

So.. if we want to have both semantics, we have to design two visually different syntax for them.
Maybe when as for binding pattern, or when === variableReference for value equality matching

There is a follow-on proposal pin operator ^ which try to solve the issue I describe.

const x = 1
case (input) {
  when ^x -> expr // same as: when 1 -> expr
  when x -> expr // could use input as x in expr
}

But I feel it does not match intuition of many js programmers.

Personally I think we'd better choose the "Matcher objects" path in that doc:

const zero = 0
case (x) {
  when 0 => ... // x === 0
  when zero => ... // same as when 0
  when range(1, 10)~x => x // 1 <= x <= 10
}
function range(from, to) {
  return {
    [Symbol.match](v) {
      return v >= from && v <= to ? v : null
    }
  }
}

In such syntax, the thing after when is always an expression (which result in a Matcher), and always use matcher~binding to create bindings. (use ~ as example, but we could consider other token, like as keyword, or use # again :)

Note, I believe such path could also solve use cases in #117

The custom matcher should know the expected pattern but I don't have a clue about how to do it.

({ [Symbol.match](value, expectedPattern) { ... } })

And how to match {done: true, value} (an iterator result object) in your (@hax ) example by ~?

I think the most intuitive solution is to evaluate parenthesized expressions prior to pattern matching, then always shadowing variables. Parenthesized expressions in JS (regardless of this proposal) are inherently evaluated prior to their parent expression, so this works with developers' natural intuition. Further, all remaining variables are bound (shadowed) in alignment with developers' natural intuition from destructuring.

const OK = 200;

case (res) {
  { statusCode: (OK) } -> ..., // matches {statusCode: 200} and does not bind any new variables
  { statusCode: OK } -> ..., // OK is introduced as shadowed variable
}

What if you'd have to explicitly declare the variables that can be used inside the matchers?

const OK = 200

case (res; const ok = OK) {
  when { statusCode: ok } -> ..., // matches {statusCode: 200} and does not bind any new variables
  when { statusCode: OK } -> ..., // OK is introduced as shadowed variable
}

@chriskuech what would happen if

const CODES {
   success: 200,
   authError: 403
}

case (res) {
  { statusCode: CODES.success } -> ..., // CODES.success is overriden?
}

@hax No I meant the initialization part there (const ok = OK) would create its own scope for the case block, like the initialization part of for-loops. In your example the inner statusCode: ok would still just mean statusCode: 200 because that's what ok refers to in that scope.

But yeah maybe that'd be too weird anyway. I guess aliasing and referring to outer variables should look properly different from each other or it would be confusing whether a line is doing one or the other.

@noppa Sorry I missed the const ok = OK part, so I deleted my previous comment.

But yeah maybe that'd be too weird anyway. I guess aliasing and referring to outer variables should look properly different from each other or it would be confusing whether a line is doing one or the other.

Totally agree!


Recently I consider the identifier confusing problem (for example, null is pattern but undefined is binding) again and again, and my conclusion is there are only two ways to solve the issue:

  1. Introduce special syntax for expressions, for example pin op ^Infinity, or just use parens (Infinity). To eliminate any potential risk of code refactor, we'd better also enforce using such syntax for literals (aka. u have to write (null) -> ..., (0) -> ..., ("x") -> ...) which is a little bit noisy. Unfortunately programmers may still write NaN -> ..., so we eventually need another new linter rule to save us 馃槦

  2. Change syntax of creating binding, instead of [start, (Infinity)] -> ..., use [?start, Infinity] -> .... Instead of {status: 200, body} -> body, use {status: 200, ?body} -> body. Personally I feel this is simple and future-proof.

    • It allow {status: OK, ?body} -> ... or {status: HTTP_CODE.OK}, ?body} -> ... or any expression.
    • It just solve the as problem
      js { type: 'A' } -> ... { type: 'A' } ?a -> a { type: 'A' ?t } -> t { ?type: 'A' } -> type
    • It allow us introduce matcher protocol so we can write { ?status: range(200, 299), ?body } -> process2XX(body, status). It also allow us introduce Java/C# instanceof/is -like pattern match: Number ?n -> n. We could also make regexp literal work in the future: /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/ -> {year, month, day}
    • The syntax now have obvious difference with destructuring, so #160 just vanish. (actually I don't think we can really unify pattern matching and destructuring syntax/semantic without sacrifice of usefulness.)

But a pattern match is not an arrow function; it may have a guard. The pattern is a type with bindings; the guard is an expression. Allowing a guard expression in a pattern match case is needed to make pattern matching useful.

Conversely, extending arrow functions to take a guard doesn't make sense outside of a decision-list context.

Python 3.10 now has pattern matching!
https://docs.python.org/3.10/whatsnew/3.10.html#pep-634-structural-pattern-matching

What is the difference between case-when statements and switch-case statements? Just in order to support destructuring when checking cases?

@aleen42 there's a number of differences. This proposal repo will be updated in the near future, and the differences should be apparent.

The proposal has been updated in #174, and as such, this issue seems no longer relevant.

If you disagree, taking into account the updated proposal, please file a new issue.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

skfreddo picture skfreddo  路  6Comments

Haroenv picture Haroenv  路  6Comments

michaelficarra picture michaelficarra  路  8Comments

JLHwung picture JLHwung  路  3Comments

arcanis picture arcanis  路  6Comments