Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Idea to reenable match expressions #116

Closed
littledan opened this issue Jun 21, 2018 · 17 comments
Closed

Idea to reenable match expressions #116

littledan opened this issue Jun 21, 2018 · 17 comments

Comments

@littledan
Copy link
Member

littledan commented Jun 21, 2018

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.
@ljharb
Copy link
Member

ljharb commented Jun 21, 2018

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

@littledan
Copy link
Member Author

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

@kevinbarabash
Copy link

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

@dead-claudia
Copy link

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.

@littledan
Copy link
Member Author

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

@dead-claudia
Copy link

@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") },
}

@devsnek
Copy link
Member

devsnek commented Nov 24, 2018

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);
  },
};

@dead-claudia
Copy link

@devsnek Beat you to the punch. 😉

@FireyFly
Copy link

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).

@dead-claudia
Copy link

@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.)

@dead-claudia
Copy link

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.

@FireyFly
Copy link

FireyFly commented Mar 2, 2019

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

@dead-claudia
Copy link

@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.

@zkat
Copy link
Collaborator

zkat commented Mar 27, 2019

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

@zkat
Copy link
Collaborator

zkat commented Jun 14, 2019

This is implemented in #140

@ljharb
Copy link
Member

ljharb commented Jun 14, 2019

#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?

@zkat
Copy link
Collaborator

zkat commented Jun 15, 2019

Oh! You're right. I thought I'd bundled them together. Turns out I Just Did™ the changes directly: 9434e14

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

@zkat zkat closed this as completed Jun 15, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants