Proposal-pattern-matching: Idea to reenable match expressions

Created on 21 Jun 2018  Â·  17Comments  Â·  Source: tc39/proposal-pattern-matching

The current proposal makes pattern matching based on anything after the -> being a statement, and the whole case construct is a statement. The motivation for this limitation is largely to avoid the previous iteration's use of blocks in statement context, which would raise all the questions of do expressions (most controversially, the use of completion values).

I think we could still support match expressions without involving completion values. The idea here would be to make the thing after -> have a similar grammar to the thing after the =>: It can either be a block or an expression. If all when clauses in a case construct are expressions, then the whole thing can be used as an expression. Object literals on the right hand side of -> need to be parenthized, as with =>.

Examples

From the README which work well as expressions:

function todoApp (state = initialState, action) {
  return case (action) {
    when {type: 'set-visibility-filter', filter: visFilter} ->
      ({...state, visFilter})
    when {type: 'add-todo', text} ->
      ({...state, todos: [...state.todos, {text}]})
    when {type: 'toggle-todo', index} -> ({
        ...state,
        todos: state.todos.map((todo, idx) => idx === index
          ? {...todo, done: !todo.done}
          : todo
        )
      })
    when {} -> ({}) // ignore unknown actions
  }
}

// The following just works now, whereas it previously required some kind of extension
<Fetch url={API_URL}>{
  props => case (props) {
    when {loading} -> <Loading />
    when {error} -> <Error error={error} />
    when {data} -> <Page data={data} />
    when _ -> throw new Error('badmatch')
  }
}
</Fetch>

const getLength = vector =>  case (vector) {
    when { x, y, z } -> Math.sqrt(x ** 2 + y ** 2 + z ** 2)
    when { x, y } -> Math.sqrt(x ** 2 + y ** 2)
    when [...etc] -> vector.length
  };
getLength({x: 1, y: 2, z: 3}) // 3.74165

(Sorry, some of the following examples are pretty stupid)

As a statement, case would permit a mixture of blocks and expressions (why not?):

case (x) {
  when 1 -> { throw "oh no!"; }
  when 2 -> console.log("oh no!")
}

As an expression, no blocks would be permitted on the right side of ->, e.g., the following would be a syntax error:

// SyntaxError
let a = case (x) {
  when 1 -> { throw "oh no!"; }
  when 2 -> console.log("oh no!")
}

Whereas it would not be a syntax error if all of them are expressions:

// Runs just fine
let a = case (x) {
  when 1 -> (() => { throw "oh no!"; })()
  when 2 -> console.log("oh no!")
}

Relatedly, return can only be used within a block, not loose within ->:

case (x) {
  when 1 -> return x  // SyntaxError
  when 2 -> { return x; } // This line is good
}

Future evolution

  • With do expressions: This composes great with do expressions: you can do when 1 -> do { } and go from there.
  • With everything becoming an expression: I'm not sure if this will happen, given @bakkot's recent veto of the first step of this process, but if it does, it also composes great: You can use any of those new kinds of expressions right after ->. Presumably, as part of the end game, we'd also relax things to allow you to use blocks in case expressions, with do expression semantics.

Most helpful comment

This is fantastic and I'll be updating the spec to add the expression form! Thank you!

All 17 comments

How does this impact using break or continue in when blocks? (which i believe wa the motivation to change them to statements)

@ljharb In the above proposal, blocks in case statements are only supported in statements, not in expressions. Therefore, the problem does not arise.

I'm really happy to see this. I worry about adding more statements to an already statement heavy language.

Just a thought: if you made the values only non-sequence expressions, you could gain everything lost by just leveraging do expressions to let it all fall out naturally. This would also make it very easy to just rip out the statement form without sacrificing functionality - you could always use it as an expression statement like people already do quite frequently for some expressions (like function calls). I've also seen a few cases of people using logical expressions as statements in the wild in open-source code, usually of the form of f() && g() && h(); or f() || g() || h();.

For what it's worth, a bare parenthesized do expression as a statement (i.e. (do { ... });) is exactly equivalent to a block statement, and could even be desugared as such.

@isiahmeadows Agreed that this should combine well with do expression, but it also doesn't depend on them.

@littledan I'm implying it could be simplified by depending on them for returning statements. Alternatively, you could make it like arrow functions and do either a block or an expression (and IIRC this variant has precedent in Rust). Something like this:

case (x) {
    when 1 -> expr,
    when 2 -> { /* block */ },
}

This could be useful in both statement contexts (obvious) and expression contexts like this:

let value = case (x) {
    when {value} -> value,
    when _ -> { throw new Error("fail") },
}

what if the value had to be an expression, and people could use do expressions to put statements in? it feels like we're missing a lot with the current iteration, especially to those of us coming from other languages with pattern matching.

const value = case(x) {
  { t } -> v,
  [asdf] -> do {
     console.log('stuff');
     thing(5);
  },
};

What's the status of this suggestion? It seems to be broadly supported, judging by the above, so it would be nice to update the spec text to match. :)

I think it'd be a pity for expressions in branches not to be supported in a pattern-matching proposal, since the motivation of pattern matching stems from functional principles and I suspect the expression form is the most important in that context (the Redux example in the README illustrates this well).

@FireyFly There is language precedent for making it statement-based: Swift. Their switch is traditional typed pattern matching, but you can still explicitly fall through to subsequent cases and it has the C-like syntax of case Foo: ... for its cases. (It does break by default.)

I've got an alternate idea: what if you separated the pattern matching statement from the pattern matching expression? Specifically:

  • case statements can have either expression or -> { block } bodies, but the expression bodies are not allowed to be objects. (These are unlikely to exist anyways except potentially in future do expressions.)
  • case expressions can only have expression bodies.

This would dodge the do expression dependency while still allowing both variants.

@isiahmeadows isn't that precisely the suggestion in the topmost post in this issue?

@FireyFly Partially.

  • At statement-level, yes, it works identically to arrow functions.
  • At expression-level, it's like the original proposal that lacked block body support.

This is fantastic and I'll be updating the spec to add the expression form! Thank you!

140 seems to be about let/const/var vs when; it might be helpful to have the expression stuff implemented in a separate PR for comparing/contrasting?

Oh! You're right. I thought I'd bundled them together. Turns out I Just Didâ„¢ the changes directly: https://github.com/tc39/proposal-pattern-matching/commit/9434e145919955c1c9567993bb64857c0be7a255

So I'm just gonna close this one. Cheers~

Was this page helpful?
0 / 5 - 0 ratings

Related issues

skfreddo picture skfreddo  Â·  6Comments

aem picture aem  Â·  6Comments

xtuc picture xtuc  Â·  4Comments

elijahdorman picture elijahdorman  Â·  6Comments

JLHwung picture JLHwung  Â·  6Comments