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

Switch conditional block #49

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

TheBlueOompaLoompa
Copy link

@TheBlueOompaLoompa TheBlueOompaLoompa commented Apr 30, 2021

0000-template.md Outdated Show resolved Hide resolved
@jpaquim
Copy link

jpaquim commented May 4, 2021

Previous discussion concerning this: sveltejs/svelte#530

About the motivation, at least for me runtime performance is a non-concern in terms of switch vs if/else, it only seems to be relevant in micro-benchmarks, or in situations where there's a disproportionate amount of switch cases.

The real win for me comes in the form of readability, and to some extent writability. Given that there exists significant precedent for switch/match statements in most popular programming language families, it's a common enough idiom that even a relatively junior developer would have been exposed to it, there is no surprise factor involved (which often comes with this kind of syntactic-sugar proposals).

Lower expectations for speed improvements
@TheBlueOompaLoompa
Copy link
Author

I have updated the RFC to lower expectations of speed improvements as you suggested, although I did also write that it may improve performance on lower end hardware.

@mossaiby
Copy link

mossaiby commented May 4, 2021

As I have already commented on sveltejs/svelte#530 the argument that there should be less things for people to learn, can be applied to any language that supports switch statement (even JS itself), as it can be handled with if/then/else if. The statement is there for a good reason: clear and less cluttered code. In fact, we may argue that people already have the pattern in their mind, so there is almost no overhead.

@kevmodrome
Copy link

I like this, I also think the syntax looks good!

@TheBlueOompaLoompa
Copy link
Author

TheBlueOompaLoompa commented May 4, 2021

Quick question which syntax does everyone prefer

(I prefer this one)

{#switch (condition)}
  <Tags that show if no cases are met>
{:case (expression1)}
  <Tags that show if condition == expression1 />
{:case (expression2)}
  <Tags that show if condition == expression2 />
{/switch}

or

(I don't like this one as much but it's more explicit)

{#switch (condition)}
{:case (expression1)}
  <Tags that show if condition == expression1 />
{:case (expression2)}
  <Tags that show if condition == expression2 />
{:default}
  <Tags that show if no cases are met>
{/switch}

Thumbs up for top, and thumbs down for bottom.

@mossaiby
Copy link

mossaiby commented May 4, 2021

I vote for the first one; even though the second one is as good.

@wstabosz
Copy link

wstabosz commented May 4, 2021

I'm more familiar with #2. Do any other programming languages use #1?

I'm inclined to vote for #1, since it's less to write, but I anticipate one day cursing a block of broken code because I normally write switch blocks in the format of #2 .

Maybe the mental context tool will be to remember that I'm writing template code, not JS.

@TheBlueOompaLoompa
Copy link
Author

TheBlueOompaLoompa commented May 4, 2021

I don't know any other programming languages that use 1 but it would be cleaner, 2 matches the typical implementation more, but maybe it could be both ways somehow. It would just describe 2 in the tutorials, but mention that 1 is also an option, as well as not having a default at all by not puting anything in the default spot for 1 or 2 (with 2 you wouldn't add {:default}) and just not show anything if neither cases are true.

@TheBlueOompaLoompa
Copy link
Author

When I have time I think I will start working on making this new feature, but I'm not very familiar with the svelte source code so it may take me a while or I may just not be able to do it.

@kevmodrome
Copy link

When I have time I think I will start working on making this new feature, but I'm not very familiar with the svelte source code so it may take me a while or I may just not be able to do it.

I'd wait for a go-ahead from some maintainer before you start building it

@TheBlueOompaLoompa
Copy link
Author

TheBlueOompaLoompa commented May 4, 2021

When I have time I think I will start working on making this new feature, but I'm not very familiar with the svelte source code so it may take me a while or I may just not be able to do it.

I'd wait for a go-ahead from some maintainer before you start building it

Ok I will wait then, but if it doesn't go through I may just make a fork for small personal projects where it won't matter as much like maybe a 1 page blog or something.

@wstabosz
Copy link

wstabosz commented May 4, 2021

I don't know any other programming languages that use 1 but it would be cleaner

I would stick with the coding style know by most developers. I would argue cleaner syntax isn't helpful if it is more error prone.

but maybe it could be both ways somehow

I think coding the Svelte compiler to support both 1 & 2 would introduce extra complexity to the source. But this is me speaking with no knowledge of how the compiler is written.

A grammatical analogy

To me, it makes sense for the default state to be written after all the well-defined states. default, means (to me) "if no choice is provided, then perform this action by default".

Imagine an employee instruction manual for how to make a sandwich for a customer

How to make a sandwich
Ask the customer what kind of bread they want:
If they say "white", get two sliced of white bread.
If they say "rye", get two slices of rye bread.
If they say "wheat", get two slices of wheat bread.
default: If they ignore you, get two slices of moldy bread.

To me, it's easier to read/comprehend the instructions with the default behavior placed at the bottom of the instructions.

@ryanatkn
Copy link

ryanatkn commented May 4, 2021

In the alternatives sections, is it worth discussing pattern matching as an alternative to copying JS's switch? Svelte could offer more powerful semantics than {:case *constant expression*}, or something else, without losing perf or other tradeoffs, I think, e.g. it could compile to switch when possible. (here's the TC39 pattern matching stage 1 proposal as a reference (also rescript), maybe Svelte could do its own thing)

@mossaiby
Copy link

mossaiby commented May 4, 2021

When I have time I think I will start working on making this new feature, but I'm not very familiar with the svelte source code so it may take me a while or I may just not be able to do it.

I tried it already, but couldn't do that because of my lack of knowledge of the source code. Doesn't seem to be so hard for the developers, though.

@jstrother
Copy link

jstrother commented May 5, 2021

I went back and forth a few times on the syntax. I really like the cleanliness of the first example, but feel that keeping things familiar makes the second one the winner.

@coyotte508
Copy link

I went back and forth a few times on the syntax. I really like the cleanliness of the first example, but feel that keeping things familiar makes the second one the winner.

Svelte makes several bold changes - with await inside template, $ to deref stores, $: for reactivity, unwrapped javascript variables directly accessible in the template - it makes me err towards the first solution.

@paulovieira
Copy link

At first glance this doesn't seem like a good idea. What value would it add? Svelte would have 2 ways to do the same thing, which (in principle) is not good.

But after reflecting a bit more, a {#switch} block might be useful when the comparison expression in the {#if} block is the equality (which I would say is the common case).

Some thoughts:

  1. One of the advantages of the switch statement is that we can "fall-through" several cases (example: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch#methods_for_multi-criteria_case). It would be interesting if this feature is also available here.

  2. One of the confusing things about the switch statement is that the break statement is usually needed but is frequently forgotten. How should we handle it here? Add yet another {:break} block?

  3. How to make the comparison? Strict (===) or loose (==)?

@jpaquim
Copy link

jpaquim commented May 5, 2021

Concerning the comparison, I'd say stick with existing JS semantics concerning == vs ===, i.e. a switch statement should be based on strict equality ===.

Regarding fall-through, I think not having any fall-through is a good compromise to avoid excessive syntax for the most common use-case (not having fall-through). This also eliminates the need for an explicit {:break} block, as break is just the default behavior for every case branch.

If fall-through is desired, I would go with something like Zig's switch statement, and use comma-separated values:

{#switch (condition)}
  <Tags that show if no cases are met>
{:case (expression1)}
  <Tags that show if condition == expression1 />
{:case (expression2), (expression2FallThrough), ...}
  <Tags that show if condition == expression2 />
{/switch}
{#switch (condition)}
{:case (expression1)}
  <Tags that show if condition == expression1 />
{:case (expression2), (expression2FallThrough), ...}
  <Tags that show if condition == expression2 />
{:default}
  <Tags that show if no cases are met>
{/switch}

This also ties in with the proposal for a more powerful {#match} expression as suggested by @ryanatkn, although it implies (as of yet) non-standard JS semantics.

@mossaiby
Copy link

mossaiby commented May 5, 2021

One of the confusing things about the switch statement is that the break statement is usually needed but is frequently forgotten. How should we handle it here? Add yet another {:break} block?

There will be no {:break} block, as is shown above; it is implicit in the syntax.

@TheBlueOompaLoompa
Copy link
Author

One of the confusing things about the switch statement is that the break statement is usually needed but is frequently forgotten. How should we handle it here? Add yet another {:break} block?

There will be no {:break} block, as is shown above; it is implicit in the syntax.

Also this part of svelte is a markup langauge, not a programming language. I do see some merit to this with adding an {#if} to hide the rest with a break like this,

{#switch (condition)}
  <Tags that show if no cases are met>
{:case (expression1)}
  <Tags that show if condition == expression1 />
  {#if (condition 2)}
    {:break} <!-- This could also have (condition 2) integrated right into it -->
  {/if}
  <Tags that show if condition == expression1 && condition2 is met />
{:case (expression2), (expression2FallThrough), ...}
  <Tags that show if condition == expression2 />
{/switch}

but then again you could just use the {#if} to hide the rest of the tags without the need for a break, I also think requiring it for each line wouldn't work because it would just add unnecessary complexity and make it even more bloated than using {#if} and {:else if}.

@mossaiby
Copy link

mossaiby commented May 5, 2021

One of the confusing things about the switch statement is that the break statement is usually needed but is frequently forgotten. How should we handle it here? Add yet another {:break} block?

There will be no {:break} block, as is shown above; it is implicit in the syntax.

Also this part of svelte is a markup langauge, not a programming language. I do see some merit to this with adding an {#if} to hide the rest with a break like this,

{#switch (condition)}
  <Tags that show if no cases are met>
{:case (expression1)}
  <Tags that show if condition == expression1 />
  {#if (condition 2)}
    {:break} <!-- This could also have (condition 2) integrated right into it -->
  {/if}
  <Tags that show if condition == expression1 && condition2 is met />
{:case (expression2), (expression2FallThrough), ...}
  <Tags that show if condition == expression2 />
{/switch}

but then again you could just use the {#if} to hide the rest of the tags without the need for a break, I also think requiring it for each line wouldn't work because it would just add unnecessary complexity and make it even more bloated than using {#if} and {:else if}.

Maybe this is what Rich Harris is worried about... simply invert the condition in the {#if} block and you do not need the {:break}. Also, using {:break} in a context that people mentally think different about it is very bad, IMO.

@paulovieira
Copy link

I actually see fall-through as a key feature for a new {#switch} block. It would improve readability over {#if}:

{#if variable === 'a'}
    <ComponentA />
{:else if variable === 'b' || variable === 'c'}
    <ComponentB />
{:else if variable === 'd'}
    <ComponentD />
{:else}
    <ComponentZ />
{/if}

vs

{#switch variable}
{:case 'a'}
    <ComponentA />
{:case 'b'}
{:case 'c'}
    <ComponentB />
{:case 'd'}
    <ComponentD />
{:else}
    <ComponentZ />
{/if}

Yes, the break statement doesn't even make sense in the markup context, forget about it.

But going this route, we might as well consider ideas from the pattern-matching proposal, as others have mentioned: https://github.com/tc39/proposal-pattern-matching

<script>
let user = { status: 'pending' }
</script>

{#match res}
{:when { status: 'pending' }}
    <Pending />
{:when { status: 'approved' }}
    <Approved />
{:when { status: 'rejected' }}
    <Rejected />
{:else}
    <Error />
{/match}

The C# language recently added a bunch of new features for pattern matching. Might be used as reference / inspiration too: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns

@mossaiby
Copy link

mossaiby commented May 6, 2021

{#switch variable}
{:case 'a'}
    <ComponentA />
{:case 'b'}
{:case 'c'}
    <ComponentB />
{:case 'd'}
    <ComponentD />
{:else}
    <ComponentZ />
{/if}

Too verbose for me. A comma separated list is much better, nicer and cleaner. Also else doesn't belong here, IMO. Either the default case should go after {#switch} itself (preferred) or {:default} can be used.

@coyotte508
Copy link

One thing to consider, for the first switch syntax:

{#switch val}
  default value
{:case 'a'}
  ...
{:case 'b'}
...
{/switch}

It mirrors the #await syntax:

{#await promise}
  default value while loading
{:then result}
  ...
{:catch error}
 ...
{/await}

@paulovieira
Copy link

Also else doesn't belong here, IMO. Either the default case should go after {#switch} itself (preferred)
or {:default} can be used.

Using a {:else} block (instead of a {:default} block) would make sense because it's already used with that spirit in other contexts. Example:

{#each todos as todo}
	<p>{todo.text}</p>
{:else}
	<p>No tasks today!</p>
{/each}

And placing the default case immediately after {#switch} would be very confusing in my opinion. We would be borrowing a concept from javascript where the default case is almost always placed at the end. Makes to no sense to have it in other place here.

@mossaiby
Copy link

Using a {:else} block (instead of a {:default} block) would make sense because it's already used with that spirit in other contexts. Example:

{#each todos as todo}
	<p>{todo.text}</p>
{:else}
	<p>No tasks today!</p>
{/each}

And placing the default case immediately after {#switch} would be very confusing in my opinion. We would be borrowing a concept from javascript where the default case is almost always placed at the end. Makes to no sense to have it in other place here.

There is no each construct in JavaScript, but there is switch there. I have no strong objection to using {:default} though; but as mentioned earlier, it is in par with {#await} syntax already used.

@mossaiby
Copy link

Can maintainers comment on this RFC? There are some people here that think it is very nice to have this feature. Also, it seems that the syntax is (almost) agreed on. Will it be implemented? Should we try to implement it ourselves and make a PR?

@madeleineostoja
Copy link

Just wanted to chime in strongly in favour of the simple switch/case/default syntax:

{#switch condition}
{:case expression1}
  <Tags that show if condition == expression1 />
{:case expression2}
  <Tags that show if condition == expression2 />
{:default}
  <Tags that show if no cases are met>
{/switch}

This matches the JS switch statement that frontend developers will be most familiar with, and I think any argument for conciseness or flexibility of other syntaxes is outweighed by the "inventing a new language" problem. We already have if/else blocks if you need more robust condition matching, a switch statement is pure sugar for highly readable and concise conditionals based on a single condition.

My personal use case: I use Prismic as my CMS for all Svelte sites, and their 'slices' use conditionals based on a type heavily, the current if/else syntax is verbose and prone to error.

@Florian-Schoenherr
Copy link

"everyone knows switch-cases!"
...but maybe it's not a good feature inside markup?

Maybe we should also look into some similar mechanisms (match?), I don't really think we need that much writing.
As @paulovieira said, fall-through should also be considered, seems to me like a real feature.

@samclaus
Copy link

samclaus commented Jul 7, 2021

At first I was strongly in favor of the syntax matching the {#await ...} block where the default case is any markup located before the {:case ...} clauses. It's less verbose and more Svelte-y. After seeing people write up some dummy examples though, I have to say, despite being more verbose, a {:default} clause really pops a lot more and is easier to read.

On the topic of switch vs. match constructs and the break keyword in popular language--and I think I speak for quite a few people when I say this: f**k fallthrough. It's caused me nothing but bugs. I've written a lot of code and I've only seen two uses for fallthrough:

  1. You want to handle several "cases" with the same code, as in:

    switch (animalType) {
       case "cat":
       case "dog":
           isMammal();
           break;
    
       case "snake":
           isReptile();
           break;
    }

    ...but this is already handled much more elegantly by match constructs because matching on multiple concrete values is accounted for in the design, with | or , delimited literals (or even runtime expressions):

    match (animalType) {
       when ("cat" | "dog") {
           isMammal();
       }
       when ("snake") {
           isReptile();
       }
    }
  2. An extremely, extremely esoteric case where you need to clean up steps in reverse-order. I don't even remember where I saw this but I remember it was C code:

    // Mind.blown()
    switch (stepReached) {
       case 3:
           undoStep3();
       case 2:
           undoStep2();
       case 1:
           undoStep1();
    }

    ...this is actually super elegant but not at all the common case for switch code. Further, we are talking about switch or match in the context of a declarative template language, where you will simply be matching (see?) values to template fragments.

As a matter of fact, Golang switch statements take the opposite approach to traditional switch statements: breaking after each case is implicit and you must explicitly fallthrough using the fallthrough keyword. This is one of many excellent design decisions I have seen in Go, since its creators have been around a long time and know the pain points of C (which JavaScript kindly adopted for all of us) very well.

In general, switch seems to me to be a relic of the past that has caused many problems. match expressions in Rust and functional languages feel like a breath of fresh air, since they are essentially refining the switch concept with decades more experience. Rust also elegantly did away with x ? y : z ternary expressions when it learned from FP languages.

Regarding implementation difficulty and the learning curve, I think match expressions are pretty well established and very similar amongst modern languages. However, it makes a lot of sense to wait for the TC-39 proposal mentioned earlier to either go through or die to ensure the Svelte syntax would not clash with JS. If enough people care about it though, it might be sensible to push for a very barebones {#match} that only supports === equality checks like the JS switch statement. That way, the semantics would be very simple and also familiar, while the "match" nomenclature doesn't mislead people into expecting switch fallthrough behavior. If the TC-39 proposal gets finalized, the match arm pattern-matching syntax could be upgraded to simply support more than literal values and would still be backward-compatible. That leaves just one nasty problem: we don't know what keywords the TC-39 proposal will decide on for the match arms: when or something else? Rust doesn't even have a keyword in front of each arm. Actually, the syntax would also have to be decided upon for the "or" syntax (| in the example below) which might end up conflicting with JS or multiple cases per-arm could just not be supported for the time being. The {:default} syntax might also end up clashing with whatever decision TC-39 takes...

{#match animalType}
{:when "dog" | "cat"}
    <h2>It's a mammal!</h2>
{:when "snake"}
    <h2>It's a reptile!</h2>
{:default}
    <h2>I don't know what that is!</h2>
{/match}

Maybe it's best to wait for the JS pattern matching proposal to make headway. :/

EDIT: Something else to strongly consider, and I don't know if {#if} blocks already do this in Svelte, is TypeScript type-narrowing. I make heavy use of discriminated/tagged unions in TypeScript so I get exhaustive type safety but Angular's template typechecker doesn't support type narrowing so I always have to cast away types using $any(...) which is one of the many reasons I am moving to Svelte ASAP, because views are often state machines with different information depending on which state the view is in.

@iacore
Copy link

iacore commented Jan 5, 2022

This is pain. switch statement in Javascript is not guaranteed to be compiled into a jump table as C is, since it supports arbitrary objects as the thing to match for. I don't want another keyword.

@mossaiby
Copy link

mossaiby commented Jan 5, 2022

Any progress on this? Many people really want it (me included). What is the decision process on this kind of RFCs? Is it on some ToDo list somewhere, or we should provide a PR?

@katriellucas
Copy link

I think it's about time for this to be implemented, most people agree with this feature, syntax is mostly set, and user case is well defined as plenty of us showed. I would love this for a far more cleaner component syntax.

@samclaus
Copy link

samclaus commented Aug 3, 2022

I think it's about time for this to be implemented, most people agree with this feature, syntax is mostly set, and user case is well defined as plenty of us showed. I would love this for a far more cleaner component syntax.

It does not seem like people agree on syntax at all. The RFC kind of hit a slump months ago and hasn't gotten much attention since then it seems. I for one, just pray that the end result plays nicely with TypeScript discriminated unions and control flow analysis, which is really the only way to do type-safe variant matching in TypeScript right now. That feature is absolutely critical for components which can be in several different states and have totally unrelated data (different types of variables) depending on what state the component is currently in.

@raventos
Copy link

raventos commented Aug 1, 2024

Two years have passed since the last comment. I understand most people agree this would be nice to have but there seems to be no agreement on how to implement it. What would need to happen to get this unstuck?

@hexxt-git
Copy link

this really should be a thing

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 this pull request may close these issues.