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

Proposal: Enhanced switch statements #3038

Open
1 of 4 tasks
333fred opened this issue Dec 19, 2019 · 26 comments
Open
1 of 4 tasks

Proposal: Enhanced switch statements #3038

333fred opened this issue Dec 19, 2019 · 26 comments

Comments

@333fred
Copy link
Member

333fred commented Dec 19, 2019

Enhanced switch statments

Summary

This proposal is an enhancement to the existing switch statement syntax to permit switch-expression-style arms instead of using case statements. This feature is orthogonal to #3037 and can be done independently, but was conceived of as taking #3037 switch expressions and applying them to switch statements.

Motivation

This is an attempt to bring more recent C# design around switch expressions into a statement form, that can be used when there is nothing to return. The existing switch statement construct, while very familiar to C/C++ programmers, has several design choices that can feel dated by current C# standards. These include requiring break; statements in each case, even though there is no implicit fall-through in C#, and variable scopes that extend across all cases of the switch statement for variables declared in the body, but not for variables declared in patterns in the case labels. We attempt to modernize this by providing an alternate syntax based on the switch expression syntax added in C# 8.0 and improved with the first part of this proposal.

Detailed design

We enhance the grammar of switch statements, creating an alternate form based on the grammar of switch expressions. This alternate form is not compatible with the existing form of switch statements: you must use either the new form or the old form, but not both.

var o = ...;
switch (o)
{
    1 => Console.WriteLine("o is 1"),
    string s => Console.WriteLine($"o is string {s}"),
    List<string> l => {
        Console.WriteLine("o is a list of strings:");
        foreach (var s in l)
        {
            Console.WriteLine($"\t{s}");
        }
    }
}

We make the following changes to the grammar:

switch_block
    : '{' switch_section* '}'
    | '{' switch_statement_arms ','? '}'
    ;

switch_statement_arms
    : switch_statement_arm
    | switch_statement_arms ',' switch_statement_arm
    ;

switch_statement_arm
    : pattern case_guard? '=>' statement_expression
    | pattern case_guard? '=>' block
    ;

Unlike a switch expression, an enhanced switch statement does not require the switch_statement_arms to have a best common type or to be target-typed. Whether the arm conditions have to be exhaustive like a switch expression is currently an open design question. Block-bodied statement arms are not required to have the end of the block be unreachable, and while a break; in the body will exit the switch arm, it is not required at the end of the block like in traditional switch statements.

Drawbacks

As with any proposals, we will be complicating the language further by doing these proposals. In particular, we will be adding another syntactic form to switch statements that has some very different semantics to existing forms.

Alternatives

#2632: The original issue proposed that we allow C# 8.0 switch expressions as expression_statements. We had a few initial problems with this proposal:

  • We're uncomfortable making these a top-level statement without the ability to put more than 1 statement in an arm.
  • There's some concern that making an infix expression a top-level statement is not very CSharpy.
  • Requiring a semicolon at the end of a switch expression in an expression-statement context feels bad like a mistake, but we also don't want to convolute the grammar in such a way as to fix this issue.

Unresolved questions

  • Should enhanced switch statement arms be an all-or-nothing choice? ie, should you be able to use an old-style case label and a new-style arrow arm in the same statement? This proposal takes the opinion that this should be an exclusive choice: you use one or the other. This enables a debate about the second unresolved question for enhanced switch, whether they should be exhaustive. If we decide that enhanced switch should not be exhaustive, then this debate becomes largely a syntactic question.

    • An important note about modern switch expression arms is that you cannot have multiple when clauses with fallthrough, like you can with traditional switch statements today. If this is an all-or-nothing choice, this means that the moment you need multiple when clauses you fall off the rails and must convert the whole thing back to a traditional switch statement.
  • Should enhanced switch be exhaustive? Switch expressions, where enhanced switch statements are inspired from, are exhaustive. However, while the exhaustivity makes sense in an expression context where something must be returned in all cases, this makes less sense in a statement context. Until we get discriminated unions in the language, the only times we can be truly exhaustive in C# is when operating on a closed type heirarchy that includes a catch-all case, or we are operating on a value type. And if the user is required to add a catch-all do nothing case, then purpose of exhaustivity has been largely obviated.

Design meetings

https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-08-31.md#switch-expression-as-a-statement
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-26.md#discriminated-unions

@HaloFour
Copy link
Contributor

HaloFour commented Dec 19, 2019

I like. It may also be a step towards making block expressions, which I personally would prefer over sequence expressions as they have been proposed.

For reference, here's the current preview switch expression syntax in Java 13. IIRC they plan on taking that syntax out of preview in Java 14 slated for release in March:

https://openjdk.java.net/jeps/325
https://openjdk.java.net/jeps/354
https://openjdk.java.net/jeps/361

// as statement
switch (p) {
    case 1 ,2, 3 -> System.out.println("Foo");
    case 4, 5, 6 -> {
        System.out.println("Bar");
    }
};

// as expression
String result = switch (p) {
    case 1 ,2, 3 -> "Foo";
    case 4, 5, 6 -> {
        yield "Bar";
    }
};

@iam3yal
Copy link
Contributor

iam3yal commented Dec 19, 2019

I'd really prefer this approach and keep the switch expression as is.

Should enhanced switch statement arms be an all-or-nothing choice?

Ideally, I'd love to mix them but because they differ syntactically and have different rules applied to them then I'd go with all-or-nothing.

An important note about modern switch expression arms is that you cannot have multiple when clauses with fallthrough, like you can with traditional switch statements today. If this is an all-or-nothing choice, this means that the moment you need multiple when clauses you fall off the rails and must convert the whole thing back to a traditional switch statement.

This can be automated.

@333fred
Copy link
Member Author

333fred commented Dec 19, 2019

This can be automated.

It can be automated, but it's a very ungraceful process if you find yourself in the scenario where you need multiple when clauses.

@333fred
Copy link
Member Author

333fred commented Dec 19, 2019

See also the discussion in ldm around making the existing switch expression an expression statement: https://github.com/dotnet/csharplang/blob/master/meetings/2019/LDM-2019-12-16.md#switch-expression-as-a-statement-expression.

@iam3yal
Copy link
Contributor

iam3yal commented Dec 19, 2019

@333fred It's very similar to LINQ where at times you need to transform query expression into a method syntax and sometimes even into a loop statement.

@333fred
Copy link
Member Author

333fred commented Dec 19, 2019

@333fred It's very similar to LINQ where at times you need to transform query expression into a method syntax and sometimes even into a loop statement.

I'm not saying there isn't precedent, I'm just saying that it's a very ungraceful degredation story.

Ideally, I'd love to mix them but because they differ syntactically and have different rules applied to them then I'd go with all-or-nothing.

An important note is that the differing syntax right now is due to my desire to start the discussion with exhaustive vs non-exhaustive in ldm. After we have discussion, if we come down on the non-exhaustive side my plan is to modify the syntax to be much more similar to the existing case labels, and that becomes less of a concern.

@laicasaane
Copy link

laicasaane commented Dec 20, 2019

I personally find it quite confusing, when reading the example code, I just thought of it as switch expression. I suggest we retain the case keyword, like the Java code preview @HaloFour has posted.

@gafter gafter added this to the 9.0 candidate milestone Jan 7, 2020
@0x000000EF
Copy link

@333fred , seems that mixing of value and type matching at the one level is not a good practice (may breaks possible compiler optimizations related with preffered type matching).

Possible, there is a sense to split value and type matching expressions into two kinds of switch (Java style for values, true lambda style for types).

Indeed, the following code very closely with your sample may be successfully compiled and run.

Check how into dotnetfiddle.net online compiler

o.Switch
(
	(int i) => Console.WriteLine($"o is int {i}"),
	(string s) => Console.WriteLine($"o is string {s}"),
	(string[] l) =>	{
		Console.WriteLine("o is an array of strings:");
		foreach (var s in l)
		{
			Console.WriteLine($"\t{s}");
		}
	}
);
var result = o.Match
(
	(int i) => $"o is int {i}",
	(string s) => $"o is string {s}",
	(string[] l) =>	{
		var result = "o is an array of strings:\n";
		foreach (var s in l)
		{
			result += $"\t{s}";
		}
		return result;
	}
);

@HaloFour
Copy link
Contributor

HaloFour commented Jan 8, 2020

@0x000000EF

That example falls apart when combined with all of the various kinds of patterns supported in C# 8.0, not to mention the delegate invocation adds non-trivial overhead and additional allocations.

@333fred
Copy link
Member Author

333fred commented Jan 8, 2020

@333fred , seems that mixing of value and type matching at the one level is not a good practice (may breaks possible compiler optimizations related with preffered type matching).

@0x000000EF I have no idea what you're actually saying here. This proposal changes nothing with regards to how we emit code in any fashion, it only changes syntax and some variable lifetime rules. Everything in my examples are completely possible in switch statements today, just with a different syntax.

@0x000000EF
Copy link

0x000000EF commented Jan 9, 2020

@333fred , a false alarm.

More year ago C# compiler had a problem.
For similar code

           switch (o)
           {
                      case 1: return "one";
                      case 2: return "two";
                      case int i when i > 2: return "too many";
                      case int i: return "too few";
      
                      case "abc": return "ABC";
                      case "xyz": return "zyx";
                      case string s: return default;
      
                      case null: return "is null";
                      default: return default;
           } 

it had generated optimal code something like

if (o is int) // type matching
{// value matchings
}
if (o is string) // type matching
{// value matchings
}

But simple reordering of cases

           switch (o)
           {
                      case 1: return "one";
                      case 2: return "two";
                      case "abc": return "ABC";
                      case "xyz": return "zyx";

                      case int i when i > 2: return "too many";
                      case int i: return "too few"; 
                      case string s: return default;
      
                      case null: return "is null";
                      default: return default;
           } 

had caused non-optimal if statements.

Currently I checked compiler output and seems that this problem has been fixed and the compiler automatically perform reordering for second case.

Ok, there is not the problem anymore.

@TheFanatr
Copy link

TheFanatr commented Nov 1, 2020

Wait so did my comment and suggestions on #2636 make it into this (as it was recently closed), and if not, can it be? It is referring to being able to include block bodies for "expressional" switch branches in switch expressions as well, not just switch statements, via having another => after the block for submitting the result, so blah switch { 1 => { /*lines of code*/ } => /*result*/ }. This is to avoid having to deal with the issue of reusing return or yield in such a scenario. I realize this issue may be scoped to just the switch statement, though I hope it isn't because this lack of analog needs to be addressed anyways if the statement version makes it in, but if so, should I reopen the aforementioned issue?

@YairHalberstadt
Copy link
Contributor

@TheFanatr

The area is pretty open for now, and the LDT is definitely considering whether blocks should be just on the statement form, on the expression form, or in general as expression blocks be allowed everywhere.

@333fred
Copy link
Member Author

333fred commented Nov 1, 2020

Switch expression enhancements are covered by #3037.

@Dimension4
Copy link

Lets make it like in Zig and make everything an expression:

  • imports
  • if ... else ...
  • switch ...
  • loops
  • blocks
  • type declarations
  • function declarations

@Atulin
Copy link

Atulin commented Sep 6, 2021

Oof, removed from C# 10 plans... Any chance it'll be considered for the next version?

@RikkiGibson
Copy link
Contributor

The proposal says "Prototype: Complete" at the top. Is that accurate? Is there a feature branch where a prototype lives?

@333fred
Copy link
Member Author

333fred commented Dec 13, 2021

The proposal says "Prototype: Complete" at the top. Is that accurate? Is there a feature branch where a prototype lives?

It is not complete.

@TahirAhmadov
Copy link

Meh. This really doesn't add that much value.

@zhyy2008z
Copy link

zhyy2008z commented Dec 15, 2021

Cheer up, I like it very much! Please implement it as soon as possible! The old switch statement is too annoying, especially the grammatical rules of variables declaration scope, I always need to add parentheses to create local scope. The break keyword is a bit redundant, too.

@zhyy2008z
Copy link

Maybe it would be better to use ";" instead of "," as the separator?

var o = ...;
switch (o)
{
    1 => Console.WriteLine("o is 1");
    string s => Console.WriteLine($"o is string {s}");
    List<string> l => {
        Console.WriteLine("o is a list of strings:");
        foreach (var s in l)
        {
            Console.WriteLine($"\t{s}");
        }
    }
}
switch_block
    : '{' switch_section* '}'
    | '{' switch_statement_arms ';'? '}'
    ;

switch_statement_arms
    : switch_statement_arm
    | switch_statement_arms ';' switch_statement_arm
    ;

switch_statement_arm
    : pattern case_guard? '=>' statement_expression
    | pattern case_guard? '=>' block
    ;

@marchy
Copy link

marchy commented Jun 1, 2023

Any updates on this?

We're currently using lambda helpers to replace this:

switch( authIdentity ){
case AuthIdentity.PhoneNumberIdentity phoneNumberIdentity:
	installation.SetPreAuthContextPhoneNumber( phoneNumberIdentity.PhoneNumber );
	break;
				
case AuthIdentity.MessengerIdentity messengerIdentity:
	installation.SetPreAuthContextMessengerPageScopedID( messengerIdentity.PageScopedID );					
	break;
				
default:
	throw new NotSupportedException( $"Unknown auth identity {typeof(AuthIdentity)}" ),
}

with this:

object _ = authIdentity switch {
	AuthIdentity.PhoneNumberIdentity phoneNumberIdentity => Do( () => {
		installation.SetPreAuthContextPhoneNumber( phoneNumberIdentity.PhoneNumber );
	}),
	AuthIdentity.MessengerIdentity messengerIdentity => Do( () => {
		installation.SetPreAuthContextMessengerPageScopedID( messengerIdentity.PageScopedID );
	}),
	_ => throw new NotSupportedException( $"Unknown auth identity {typeof(AuthIdentity)}" ),
}

This uses the following lambda helper (which is useful in all sorts of other contexts):

/// <summary>
/// Performs the given action, returning the 'no-op' result (fundamental C# limitation).
/// </summary>
/// <param name="action"></param>
/// <returns>NOTE: This object represents 'Void' – containing a "no result' result</returns>
[DebuggerStepThrough]
public static /*void*/object Do( Action action ){
	action();
	return new object();
}

There is some ugliness needed with the object _ = because C# doesn't let the switch expression get invoked without assigning it to an object (ie: purely for the side-effects) - which is another annoying/unnecessary limitation.

It would be fantastic having this as out-of-box language support. It's not only used for multi-line statements, but even single-line statements that invoke different logic/methods, as shown in the example.

@KennethHoff
Copy link

@marchy could this workaround be improved by this suggestion? Allowing you simply write

_ => Do({
    // Logic
    }),

#6122

@eduarddejong
Copy link

Once again I think Rust can also be a great inspiration for other languages.
Every statement in Rust is also a value returning expression. It's all very consistent and simple.

What I am talking about:
https://doc.rust-lang.org/book/ch06-02-match.html
https://doc.rust-lang.org/stable/reference/expressions/match-expr.html

Read match just as switch, it's the same concept.

It shows how a language can just have 1 single kind of construct for everything.

However, in Rust the last line where you leave the semicolon open is the one which yields a value, which I would not want to suggest for C# where as it is not idiomatic.

I would just say the yield keyword is beautiful to return values just like Java. Or maybe yield return, but that probably conflicts with the current iterator usage.

@rutgersc
Copy link

rutgersc commented Apr 7, 2024

Is there any update on this?

As @eduarddejong mentions, there are other languages that have none of these issues, where they either require everything to have a return value, or some kind of (or lack of) identifier to signal the return value.

The Do() helper is not really a decent workaround though unfortunately, might as well use the old switch statements.

@CyrusNajmabadi
Copy link
Member

@rutgersc When updates happen, these issues are updated. No need to ask :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests