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

The Syntax Bikeshedding Dojo, round 8: switch #817

Closed
yannham opened this issue Sep 6, 2022 · 13 comments · Fixed by #970
Closed

The Syntax Bikeshedding Dojo, round 8: switch #817

yannham opened this issue Sep 6, 2022 · 13 comments · Fixed by #970

Comments

@yannham
Copy link
Member

yannham commented Sep 6, 2022

Switch

We have an unloved construct in Nickel, the switch. It's currently not as useful as it could be, since it's just a C-like switch that works only on enum tags. However, it could become a nice tool in the future, if we allow to match against patterns, literals, etc.

Issues

However, there are a few quirks:

  1. The current syntax is odd: we first write the cases, and only then the expression that is matched. This structure makes the flow hard to follow, in my humble opinion:

    switch {
      `foo => 1,
      `bar => 2,
       _ => 3
    } some_exp
    
  2. switch is reminiscent of C or JavaScript (to be fair, it is currently close to those operators in semantics), but isn't common in functional languages with pattern matching. If we are to implement pattern matching, we should probably use a different keyword.

Proposals

Note that because of the ML-y syntax of Nickel, we can't just swap the order of the cases and of the argument. Otherwise, we can't easily disambiguate between a record that is an argument to a function application and the cases of a switch (well we can right now, but probably won't be able in the future using an LR(1) parser without hacky backtracking, eg if we allow to match on string literal or to bind simple variables like "x") :

# should we parse this as `switch (f {...}) { cases })` or `(switch f { cases...}) {...}`? 
switch f {...} {...}

One common way to disambiguate is to add a keyword between the expression and the cases. For example:

  • OCaml has with: match ... with ...
  • Haskell, Elm, Idris, PureScript, SML, etc. have of: cases ... of ...

I think using either of the two match ... with ... or case ... of ... is reasonable. C-like languages seems to have stuck with the switch (Java, TypeScript for the compatibility with JavaScript, C#, etc.), while elsewhere the match seems to be popular (Python, Rust, Racket, etc.). case ... of ... looks quite specific to Haskell-y or ML-y languages.

Another solution for disambiguation is to use a keyword for cases that makes a clear distinction with record literal, for example:

match expr {
  case `foo => ...,
  case `bar => ...,
}

However we pay one additional keyword for each case, while the previous solution is just one word for the whole pattern match.

@ebresafegaga
Copy link
Contributor

I think just match ... { ... } is good. match ... with ... or case ... of ... are used mostly in languages that don't really use curly braces for delimiting scopes or other "sections."

@Radvendii
Copy link
Member

Is it possible to have the switch statement be more like a normal function that takes in a value and a record? Kind of like in Nix we use

{
  foo = 1;
  bar = 2;
}.${x}

Sorry if this is outside the scope of this proposal and / or doesn't make sense. Feel free to ignore.

@aspiwack
Copy link
Member

I like the word switch as it reminds that it's not a full blown pattern matching but a choice between alternative.

We have a number of alternatives for the grammar (this is orthogonal to switch vs match vs case).

  • We can ask that in switch u {…}, u is at a tighter precedence than application. Then if we need an application, we need parentheses. (that being said, applications tend to have very tight precedence, so it may mean that u is always an identifier, or needs to be parenthesised)
  • We can add a keyword switch u between {…}, switch u among {…}. switch u with {…}
  • We can have u before the keyword u switch {…}
  • We can use a different kind of delimiter switch u {{…}} (I guess using a herald for each alternative falls under this bullet point as well)

I'm not sure what I like best.

@yannham
Copy link
Member Author

yannham commented Oct 13, 2022

I like the word switch as it reminds that it's not a full blown pattern matching but a choice between alternative.

But the question is, is meant to stay that way? I feel like if we have a syntax for destructuring pattern, we may introduce pattern matching as well (although I'm well aware that while it's natural on the user side, the implementation of (efficient) pattern matching is more complex than plain destructuring)

We can ask that in switch u {…}, u is at a tighter precedence than application. Then if we need an application, we need parentheses. (that being said, applications tend to have very tight precedence, so it may mean that u is always an identifier, or needs to be parenthesised)

Yes. My personal, unscientific and unsubstantiated impression is that in functional languages you often match on the result of an application, so this is a tad annoying. That being said, if we stick to a switch-for-enum and not pattern matching, then it may be different.

We can have u before the keyword u switch {…}

@arobertn indeed mentioned elsewhere that Scala does this (pattern matching in Scala). This also looks reasonable.

@yannham
Copy link
Member Author

yannham commented Oct 13, 2022

Is it possible to have the switch statement be more like a normal function that takes in a value and a record? Kind of like in Nix we use

Right now the syntax for switch cases and a normal record is already different, but this is just an arbitrary choice. However if we are to add pattern matching then it becomes obviously impossible.

@yannham yannham added this to the Next minor (0.3) milestone Nov 17, 2022
@sir4ur0n
Copy link
Contributor

sir4ur0n commented Nov 18, 2022

Random thoughts and remarks, even though I'm late to the party 😅

Current syntax

I agree with @yannham that the current syntax switch {<cases>} value looks clunky and is a tad hard to follow.
But this is not really different from inlining a function, right? E.g. I find (fun a b => a + b) 1 2 difficult to read too.
On the other end, I find

let
  add = fun a b => a + b
in 
  add 1 2

decently easy to read.
More on that below.

Switch vs pattern matching

I like the word switch as it reminds that it's not a full blown pattern matching but a choice between alternative.

But the question is, is meant to stay that way? [..]

I concur with @yannham : pattern matching is strictly more powerful than switching, so if we stick with switch before committing to 1.0, we may regret this decision later as we want to introduce full blown pattern matching.
I think it is not that confusing for users to see a match syntax that does not (yet) pattern match, but only matches on values. It's not like the keyword is lying.

Switching/Pattern matching as a function

I (think I) partially agree with @Radvendii 's idea/suggestion: IMO a switch or pattern matching can always be considered as a function taking the expression to match on and returning a value, right?
I'm not so thrilled about the record part of @Radvendii 's suggestion though 😅
If I can create a switch/pattern matching without the value to match on then why would we treat it differently from regular functions?
E.g. even with the current syntax, I would argue that

switch {`foo => true, _ => false}

should be a valid Nickel expression: it's a function.

Do we even need a keyword?

I am not advocating to remove the keyword, be it switch, match, or another, but I'm surprised this idea was not at least discussed: do we need a keyword? What if the syntax {case1 => value1, case2 => value2} was enough to create a switch/pattern matching function?

let
  is_foo = {`foo => true, _ => false}
  # or
  is_foo: [|`foo, `bar`, `baz|] -> Bool = {`foo => true, _ => false}
in
  is_foo `bar

Note: I am not arguing that the keyword-less syntax for switch/pattern matching should be {case1 => value1, [..]}; there may be a better, cleaner, less-ambiguous-with-regards-to-lambdas-or-records syntax.

@yannham
Copy link
Member Author

yannham commented Nov 30, 2022

We had another discussion about this. The idea of being able to "curry" pattern matching (asked a long time ago in #477) is popular, to allow users o define functions matching on their argument in a direct and natural way. Together with the reverse application pipe operator, we can achieve something similar to Scala, introducing just one key word (match). The keyword is not strictly necessary but was seen as beneficial to disambiguate record definitions from match cases for newcomers and devops bystander. Taking @sir4ur0n 's examples again, we would have:

 let
  is_foo = match {`foo => true, _ => false}
  # or
  is_foo: [|`foo, `bar`, `baz|] -> Bool = match {`foo => true, _ => false}
in
  is_foo `bar

But would also be able to match inline by just relying on reverse application:

function.id `foo |> match {`foo => 1, `bar => 2}

[EDIT: MISSING PIPE FIXED]

@aspiwack
Copy link
Member

Is function.id the identity? If so, I don't think the last line has the intended semantics.

@yannham
Copy link
Member Author

yannham commented Nov 30, 2022

Is function.id the identity? If so, I don't think the last line has the intended semantics.

Oh, right, the revapp pipe operator is missing:

function.id `foo |> match {`foo => 1, `bar => 2}

@aspiwack
Copy link
Member

Ah, makes sense. And why do you need id at all? Why not just:

`foo |> match {`foo => 1, `bar => 2}

@yannham
Copy link
Member Author

yannham commented Dec 2, 2022

It's just for the sake of having an example of matching on a function application (making the point that you don't need parentheses), but it's indeed useless semantically 🙂

@aspiwack
Copy link
Member

aspiwack commented Dec 2, 2022

Ah, the point is that application binds tighter than |>. I get it. Thanks.

@yannham
Copy link
Member Author

yannham commented Dec 16, 2022

Proposal implemented in #970

@yannham yannham closed this as completed Dec 16, 2022
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

Successfully merging a pull request may close this issue.

5 participants