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

Why when and not |? #143

Closed
goodmind opened this issue Aug 14, 2019 · 41 comments
Closed

Why when and not |? #143

goodmind opened this issue Aug 14, 2019 · 41 comments
Labels
syntax discussion Bikeshedding about syntax, not semantics.

Comments

@goodmind
Copy link

goodmind commented Aug 14, 2019

const res = await fetch(jsonService)
case (res) {
  | {status: 200, headers: {'Content-Length': s}} ->
    console.log(`size is ${s}`),
  | {status: 404} ->
    console.log('JSON not found'),
  | {status} if (status >= 400) -> {
    throw new RequestError(res)
  },
}
<Fetch url={API_URL}>{
  props => case (props) {
    | {loading} -> <Loading />
    | {error} -> <Error error={error} />
    | {data} -> <Page data={data} />
  }
}
</Fetch>

This seems fewer characters to type and avoids adding new keywords to the language

This also works naturally with or pattern

const f = x => case (x) {
   | 0
   | 1 
   | 2 -> "low precedence"
   | 3 
   | 4 -> "high precedence"
}
f(0)

Syntax inspired by ReasonML and OCaml

@goodmind
Copy link
Author

goodmind commented Aug 14, 2019

I have seen #140, and it doesn't make sense to me, because match clause isn't really destructuring, so prefixing it with let, var, const would only confuse (also if this was made to avoid adding keywords?)

@ljharb
Copy link
Member

ljharb commented Aug 14, 2019

I still object to using case; i insist on adding a new keyword, since it’s a new concept.

Using a pipe for the conditions might work, but it seems pretty opaque - code is read much more often than it’s written, so I’d prefer to see us optimize for reading and understanding, not for writing.

@topaxi
Copy link

topaxi commented Aug 14, 2019

I'd rather type when or case instead of |, typing a short word keeps me more in the flow than using characters which need modifier keys (on my layout at least..). It also reads much nicer with a keyword and needs less explanations.

@kevinrodriguezGL
Copy link

I don't see how using a pipe is opaque, it's pretty expressive and natural, this is JavaScript, not Basic. Take spread operator as an example, it'll become second nature in a couple of days of using it.

@topaxi
Copy link

topaxi commented Aug 14, 2019

The pipe is already used for logical or, binary or, maybe the pipeline operator, I personally feel there's enough pipes already.

@goodmind
Copy link
Author

@topaxi Yeah, but I've seen plans for || or | anyway. So why not embrace it fully

@goodmind
Copy link
Author

@topaxi pipeline operator uses |>

@ljharb
Copy link
Member

ljharb commented Aug 14, 2019

| is still the bitwise OR operator, and is visually similar to || and |>. It also doesn't convey what it does, to someone unfamiliar with reasonml or ocaml.

@goodmind
Copy link
Author

goodmind commented Aug 14, 2019

@ljharb fair point, but I think most of users know about TypeScript/Flow unions, where it is used as OR, bitwise OR operator also should be intuitive that it is match clause OR match clause OR match clause

@goodmind
Copy link
Author

I can understand if there's ambiguity in parsing |, but is | really unfamiliar? If pipeline lands would |> also be unfamiliar?

@ljharb
Copy link
Member

ljharb commented Aug 14, 2019

@goodmind on the contrary, i'm pretty certain most JS users have never even seen a typed language.

@goodmind
Copy link
Author

goodmind commented Aug 14, 2019

@ljharb what about bitwise OR? I mean it's still OR so should be familiar enough

@kaizhu256
Copy link

most javascript-problems require rewriting your code/api so frequently that trying to lock things down with type annotation is generally anti-productive

@ljharb
Copy link
Member

ljharb commented Aug 14, 2019

the familiarity is that it coerces two operands to a 32 bit integer and produces a new one. How is that similar to your suggested usage?

@goodmind
Copy link
Author

goodmind commented Aug 14, 2019

@ljharb familiarity in name, OR, it's basically match that or that or that

@ljharb
Copy link
Member

ljharb commented Aug 14, 2019

People know semantics more than names.

@goodmind
Copy link
Author

@kaizhu256 this isn't about types

@dantman
Copy link

dantman commented Aug 14, 2019

How do you plan to parse these?

The former is 2 cases written inline, the latter is a single case with a bitwise | operator and an object literal. The second is of course nonsensical, you'd never want to do a bitwise or with an object literal; but it is still syntactically correct and needs to be differentiated.

case (props) {
  | {foo} -> foo | {bar} -> bar
}
case (props) {
  | {foo} -> foo | {foo}
}

How would you differentiate a case | from a bitwise | inside an expression inside a case? Would you treat it like other statements that require you to either use a newline or ; to separate the two (and the first would not be valid unless it was | {foo} -> foo; | {bar} -> bar? Or would you force the parser to do a long lookahead to tell the difference? i.e. when it hits the | in both do a lookahead past the ambiguous {foobar} until it hits a non-ambiguous token like -> (case), if (case), } (exp), etc.

@goodmind
Copy link
Author

@dantman is parser recoverable? I think lookahead seems most plausible, but if it is too inefficient then newline is fine for me, so no inline cases

@leggomuhgreggo
Copy link

leggomuhgreggo commented Aug 16, 2019

Symbol-heavy syntax makes things less accessible for new folks. For example, everyone gets what an if/else statement does, but ternary syntax can be really intimidating.

This feature's API is already somewhat complex. Power user ergonomics and parity with type notation + other languages aren't without merit -- I dig those points frealz -- but they are somewhat esoteric premises, and IMO it's not worth the cost to inclusiveness, especially since | has contextual nuances elsewhere in the language's API.

I strongly favor when in this case*, but could withhold protest if | were considered as a shorthand.

Thanks!

Edit: *Oh boy, no pun intended

@jrista
Copy link

jrista commented Aug 21, 2019

I think @dantman nailed the key issue with using the pipe symbol for pattern matching. Parser efficiency and even just how the parser would differentiate the case pipe from bitwise pipe.

If a symbol was desired, it might be worth trying to use something more like the pipeline operator:

|>

Of course, if someone then tried to use a pipeline within a matching clause, you would have similar problems. So perhaps:

||>

Interestingly, this is ligatured with Fira Code (both are, actually):

match-and-pipe

@PinkaminaDianePie
Copy link

| feels cleaner and easier to read for me than when. If we replace when by | and case by match it will be nearly ideal

@raitucarp
Copy link

what about |:?

@hitmands
Copy link

#149 also could be an idea...

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

@webdeb
Copy link

webdeb commented Nov 27, 2019

Instead of defining a single const function, and replace switch with case there should be a way of defining multiple definitions of the function itself, so the different implementations would match on the patterns

Sorry, when I see

case (x) 
  when 1 -> ...
  when 2 -> ...

Its like ifs but longer words, too much noise..

I would prefer something simpler, and easier to read and understand

fibo: 0 -> 0
fibo: 1 -> 1 
fibo: x -> fibo(x - 1) + fibo(x -2)

fibo(10)

@FireyFly
Copy link

@webdeb this issue is about the pros and cons of using | as a token instead of a keyword such as when. It's probably best to keep the issue focused on this, rather than stray into unrelated topics. Additionally, at its core the proposal is about a pattern-matching construct--I think "syntactic sugar for defining functions", whilst somewhat related, is mostly orthogonal to that.

@IceSentry
Copy link

Is there any reason why a symbol or keyword is necessary at all? For example, Rust doesn't use anything and it's been fairly easy to read. As a reader, it's pretty obvious that the pattern is on the left of the -> and the expression on the right.

Rust uses , to terminate the statement, but it could be a newline/; here.

@jrista
Copy link

jrista commented Dec 28, 2019

I think there really needs to be some kind of keyword or symbol in order for the parsing to be efficient. Parser efficiency has been a concern in other proposals as well, as js is interpreted/compiled on the fly, rather than ahead of time like Rust. This same parser efficiency concern is also part of the reason why we cannot necessarily just rely on local context to change the meaning of existing symbols, like |. Contextual meaning often requires more complex and slower parsing logic, so overloading existing operators may not be a viable solution.

@BNJHope
Copy link

BNJHope commented Jan 9, 2020

Outside of how the syntax feels ergonomically, should there be some consideration over the paradigm that this syntax is aiming at? Examples of the when/case keywords are more like imperative style branching logic, which will be more accessible to developers less familiar with functional programming, but also feels less like pattern matching you find in some other functional languages that already have pattern matching as an important part of the paradigm, such as Haskell (although parser efficiency is less of a problem for Haskell).

@ljharb ljharb added the syntax discussion Bikeshedding about syntax, not semantics. label Apr 1, 2020
@dustbort
Copy link

dustbort commented Feb 7, 2021

TL;DR Omit when. Acknowledge union types.

Like @IceSentry , I ask why when is necessary; it can be omitted and still parse. Be terse.
The second issue, which was illustrated by OP but not stated by anyone is fall-through or multiple cases in the pattern.

Two separate issues:

  1. omission of a case-commencing token; and
  2. syntax for multiple cases in the pattern (not the guard).

Now for the semantics of (2):
fall through
✔️ union type

The pattern defines a type (with bindings), and a type defines a set of allowed structures. So, it is natural to regard multiple cases as a union type, which makes it obvious why | is used there. Again, the pattern defines a type with bindings; the guard is an expression. The meaning of | in types vs expressions is distinct and clear -- see TypeScript. Then finally, the allowance of a preceding | in a union type definition is for aesthetics, as illustrated in OP.

So in the case when the union type has only a single item in the union, the preceding | is not a case-commencing token; it is a union type syntax, which is aesthetically pleasing when aligned vertically.

@ljharb
Copy link
Member

ljharb commented Feb 7, 2021

Terseness is not a virtue; clarity and understandability is. Code is written far less often than it is read, and thus optimizing for writability is an antipattern.

@PinkaminaDianePie
Copy link

Code is written far less often than it is read

That's why we prefer | over when. That's why fonts with ligatures exist. Symbols with special meaning may be harder to read when you see them for the first time in your life. But after some time it becomes easier to read symbols, than just words. Why do we use braces {} instead of begin/end blocks? why did we add arrow functions, instead of adding yet another keyword to use together with function? Using letters instead of symbols != having better readability

@oguimbal
Copy link
Contributor

oguimbal commented Feb 7, 2021

@ljharb +1 on that... Terseness for the sake of terseness is not a good idea.

But it's IMO very doubtful that bloating your code with dozens of useless whens will make it clearer and more understandable.

I too think that something shorter would be nicer to read.

That said, I'd like to see pattern matching in JS so much that I wouldnt mind any syntax 😊

@samhh
Copy link

samhh commented Feb 7, 2021

To elaborate upon @PinkaminaDianePie's comment, I think there's a faulty implication in some comments here that verbosity inherently increases readability, which is absolutely not a universal truth.

How you define it on a case-by-case basis is relatively subjective, but I think that the readability for someone who's already aware of the syntax will be better with the terser syntax. The verbose syntax is optimising for readability specifically for the unfamiliar, which is in my view questionable at best.

@devsnek
Copy link
Member

devsnek commented Feb 7, 2021

Most languages don't have a token in front of the cases at all (and sometimes | is used to join cases to a common body). Do we need to have any token here?

@ljharb
Copy link
Member

ljharb commented Feb 7, 2021

| indeed is most useful as a way to join multiple patterns, so using it for any pattern wouldn't really make sense.

Yes, there are use cases to match against an expression and also a pattern, which are two patterns that must be distinguished. There's also the scenario of a guard without a pattern, and also a default/else/fallback case - so there will have to be some kind of token.

How it's spelled isn't particularly important in stage 1 - that's a stage 2 concern.

@dustbort
Copy link

dustbort commented Feb 8, 2021

From most languages I have seen, the pattern matching takes an optional guard. This allows for the bound variables to be used in the guard's expression, to further narrow the match beyond what is possible to express with type syntax alone.

For example:

case (foo) {
  [a, b] if (a < b) -> [a, b],
}

@dustbort
Copy link

dustbort commented Feb 8, 2021

@ljharb Aesthetics is certainly important, which is why I specifically mentioned it.

@ljharb
Copy link
Member

ljharb commented Feb 8, 2021

Right but there should also be support for an if without a pattern - ie, avoiding unconditional match clutter.

@dustbort
Copy link

dustbort commented Feb 8, 2021

there should also be support for an if without a pattern

I was going to mention this, as well as the catch-all case having no pattern and no guard. Then I thought maybe it was intentional in other functional languages to have to write _ if (...) -> and _ -> in those cases.

@ljharb
Copy link
Member

ljharb commented Apr 21, 2021

The proposal has been updated in #174. It uses | as a pattern combinator, and when (<pattern>) to begin clauses (a bare guard, starting with if, is also permitted; as is else).

Please file a new issue if, taking into account the updated proposal, you have any concerns.

@ljharb ljharb closed this as completed Apr 21, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
syntax discussion Bikeshedding about syntax, not semantics.
Projects
None yet
Development

No branches or pull requests