dotnet / csharplang

The official repo for the design of the C# programming language
11.49k stars 1.02k forks source link

Champion: Switch expression as a statement expression #2632

Open gafter opened 5 years ago

gafter commented 5 years ago

I propose that we support a switch expression as a statement expression when every arm's expression is also a statement expression. No common type among the arms is required when used as a statement expression.

void M(bool c, ref int x, ref string s)
{
    c switch { true => x = 1, false => s = null };
}

Meeting notes

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

DavidArno commented 5 years ago

Just to check, since I get confused between statement expressions and expression statements, will the following syntax by valid with this proposal:

void M(bool c, ref int x, ref string s) => c switch { true => x = 1, false => s = null };
Mteheran commented 5 years ago

Is the operator for default case still the same for this scenario or this new feature??

As I understand underscore is the current operator

void M(bool c, ref int x, ref string s) { c switch { true => x = 1, false => s = null, _ => x=0 }; }

dsaf commented 5 years ago

Together with exhaustiveness checks this will render existing switch statement pretty much legacy?

CyrusNajmabadi commented 5 years ago

@dsaf how so? You still can't have things like blocks/multi-statements in an expression-form switch.

dsaf commented 5 years ago

@CyrusNajmabadi well maybe initially but those could be added going forward.

alrz commented 5 years ago

Together with exhaustiveness checks this will render existing switch statement pretty much legacy?

Non-exhaustive switches can't be written via switch expression,

void M(string s) {
    switch (o) {
        case 1: s = "A"; break; 
        case 2: s = "B"; break;
    }
}

Unless this is allowed

void M(string s) {
    o switch { 1 => s = "A", 2 => s = "B" };
}

(You still need to repeat the assignment.)

CyrusNajmabadi commented 5 years ago

@CyrusNajmabadi well maybe initially but those could be added going forward.

In that case, they do not "render existing switch statement pretty much legacy". :)

Thaina commented 5 years ago

What happen to

var a = c switch { true => x = 1, false => s = null };

?

DavidArno commented 5 years ago

@Thaina,

It seems sensible that it'll give you a error CS0815: Cannot assign void to an implicitly-typed variable

Thaina commented 5 years ago

@DavidArno We actually could write this

int x = 0;
var a = x = 1; // a is int

And in the next C# I think we could possible to

var a = condition ? 1 : null; // a become Nullable<int>

So actually I expect that it might be possible to

var a = c switch { true => x = 1, false => s = null };
// a become Nullable<int> or object or (int | string)
// then assign both x and a to 1 if true else assign both s and a to null

So I just ask to make sure

yaakov-h commented 5 years ago

If s is a string, then you get error CS8506: No best type was found for the switch expression.

If s is a Nullable<int>/int?, that already compiles, and I see no reason why that would change.

Thaina commented 5 years ago

@yaakov-h It possible that it's behaviour could be CS0815 as @DavidArno speculate. Or maybe other. That's why I ask to make sure what it really would be

As for me I hope that we should support union type and could let this syntax return union type. Or just plain object

DavidArno commented 5 years ago

@Thaina,

The value of, c switch { true => x = 1, false => s = null };, isn't ?int though; it's void. Just as I can write,

void Foo() {}
void Bar() => Foo();
void Baz()
{
    var x = Foo(); // results in error CS0815: Cannot assign void to an implicitly-typed variable
}

So the same should be true of a statement expression switch expression,

void Bar(bool c, ref int x, ref string s) 
    => c switch { true => x = 1, false => s = null }; // all good
void Baz(bool c, ref int x, ref string s)
{
    var a = switch { true => x = 1, false => s = null }; 
    // results in error CS0815: Cannot assign void to an implicitly-typed variable
}
Thaina commented 5 years ago

@DavidArno

c switch { true => 1, false => null };

This expression should be int? though. So

var a = c switch { true => 1, false => null }; // a is int?

Given that

var a = x = 1; // a is int

Expression x = 1 itself is int, not void. So even if we put it in switch it should still be int

C# also already has the ability to ignore any return type to void with => syntax

int Bar() => 0;
void Foo() => Bar();

So I don't think we need to limit switch expression to be void

DavidArno commented 5 years ago

@Thaina,

Sure, var a = c switch { true => 1, false => null }; should reasonably be expected to result in a being a ?int under proposals to support var a = c ? 1 : null;. But that's completely different to:

c switch { true => x = 1, false => s = null };

where x = 1 and s = null are statement expressions (ie have the "value" of void) and thus this proposal to allow the whole switch expression to be treated as a stament expression (ie have the "value" of void).

Thaina commented 5 years ago

@DavidArno My point is, even in today C#. The statement expressions is not void but it would be the type of the variable of that expression

This below is valid

bool b = someCondition;
if(b = true) // valid and work, it assign true to b and then check with value of b
{

}
DavidArno commented 5 years ago

@Thaina, That's a fair point. Perhaps therefore var a = switch { true => x = 1, false => s = null }; could result in a being a ?int. I was going to suggest that @gafter's example was a confusing one and that something like:

void M(bool c, ref int x)
{
    c switch { true => x = 1, false => Console.WriteLine("hello" };
}

is clearer over it being a statement expression. But this example would have masked your question, so it was a good example to choose.

Edited as @yaakov-h has pointed out that, since there's no common type between int and string, the only valid result of the expression has to be void.

yaakov-h commented 5 years ago

Just to clear things up a bit, the sample code @Thaina provided is already valid if you declare those variables correctly:

Sharplab Playground.

Thaina commented 5 years ago

@DavidArno There is common type between int and string which is object

And in the future we might have union type like a typescript. Which should be used in this scenario

As I said I just want to have clear official statement on this

gafter commented 5 years ago

If this is not too complex we'd like to include it in C# 9.0.

ronnygunawan commented 5 years ago

Is it still not a good idea to have switch expression with method body?

var x = o switch {
    Rectangle r => r.Width * r.Height,
    Circle c => {
        var radius = c.Diameter / 2; // allow multiple statements in one case
        Debug.WriteLine(radius); // allow side effects and break points
        return Math.PI * radius * radius;
    },
    _ => throw new NotImplementedException()
};

Switch expression as statement expression:

o switch {
    Foo f => {
        f.Bar();
        ...
    },
    _ => {
        ...
    }
}
DavidArno commented 5 years ago

@ronnygunawan,

Blocks aren't statement expressions, so allowing them within a switch expression would be beyond the scope of what's proposed for #2860

I personally remain unconvinced of the benefits of allowing them either. The obvious use case is for something like:

Point3D TransformTo3D(object o)
    => o switch {
        XOffset x => new Point3D(x, x, x),
        Point2D p => {
            var z = p.X + p.Y / 2;
            return new Point3D(p.X, p.Y, z);
        },
        Point3D p => p,
        _ => throw new NotImplementedException()
    };

Where we want to do some sort of assignment prior to the main expression. And this is proposed to be handled via Declaration Expressions and could be written as:

Point3D TransformTo3D(object o)
    => o switch {
        XOffset x => new Point3D(x, x, x),
        Point2D p => ( var z = p.X + p.Y / 2; new Point3D(p.X, p.Y, z)),
        Point3D p => p,
        _ => throw new NotImplementedException()
    };

Beyond that, examples of using a block in a switch expression generally look like examples of the need for a separate function to me. With your example, it could be handled by:

var x = o switch {
    Rectangle r => r.Width * r.Height,
    Circle c => AreaOfCircle(c),
    _ => throw new NotImplementedException()
};

double AreaOfCircle(Circle c)
{
    var radius = c.Diameter / 2;
    Debug.WriteLine(radius);
    return Math.PI * radius * radius;
}
HaloFour commented 5 years ago

Declaration expressions feel so odd compared to blocks, though. And local functions require you to move the logic of the operation away from where it's intended to be used.

Java currently has its own version of the switch expression in preview and they do support both expression and statement forms with multiple statements, using yield to return a result from the block:

var x = switch (shape.getType()) {
    case RECTANGLE -> {
        var rect = (Rectangle) shape;
        yield rect.getWidth() * rect.getHeight();
    };
    case CIRCLE -> {
        var circle = (Circle) shape;
        var radius = c.getDiameter() / 2;
        yield Math.PI * radius * radius;
    };
    default -> throw new IllegalOperationException();
};

That example looks kind of horrid due to the lack of pattern matching, but that is something that they plan on integrating with switch statements and expressions just as C# has.

DavidArno commented 5 years ago

@HaloFour,

And local functions require you to move the logic of the operation away from where it's intended to be used.

At the risk of indulging in a reductio ad absurdum claim, unless one puts all non 3rd-party code in main, or the equivalent, then one is always "[moving] the logic of the operation away from where it's intended to be used".

There's a balance between overly long functions that are hard to reason and so many tiny functions that the code becomes even harder to reason. But expressions are prone to becoming really hard to read really quickly, if long. And nothing will make them overly long more readily than allowing blocks within them.

To my mind, your Java example benefits from being:

var x = switch (shape.getType()) {
    case RECTANGLE -> AreaOfRectangle((Rectangle) shape);
    case CIRCLE -> AreaOfCircle((Circle) shape);
    default -> throw new IllegalOperationException();
};

Those method names, AreaOfRectangle and AreaOfCircle tell me what's happening. I'm unlikely to need to look at the method's contents to find out how it's calculating those values. So by moving the implementation details of those operations away from where they are used, I've made the code simpler, rather than more complex.

Thaina commented 5 years ago
var x = switch (shape.getType()) {
    case RECTANGLE -> {
        var rect = (Rectangle) shape;
        yield rect.getWidth() * rect.getHeight();
    };
    case CIRCLE -> {
        var circle = (Circle) shape;
        var radius = c.getDiameter() / 2;
        yield Math.PI * radius * radius;
    };
    default -> throw new IllegalOperationException();
};

Personally I am very much prefer this java syntax more than our current C# switch expression syntax. This is actually completely make sense. Using yield to return in the statements block and just let switch() be the same syntax (except -> in place of : )

I wish that we should be able to allow this statement block as a naked block for #249

Thaina commented 5 years ago

@DavidArno I am on the side of against Declaration Expressions

Because that was actually makebelieve syntax to bringing a code block that should be multiple lines, cramming it into one line and just hand wave that the last line will be returning without any specific keyword

It seemingly redundant and not what how we write C# for all these years. We always use ; as expectation that it would be the end of a line. And whenever we want multiple line of codes we always use braces {}. And the parentheses () was used only if it was actually a one line expression. Declaration Expressions going against all these common sense

The idea that switch expression must contain only expression and cannot contain statement is unexpected from the start. I think we should just accept a returnable statement syntax and let it be used in switch like Java

HaloFour commented 5 years ago

@DavidArno

And nothing will make them overly long more readily than allowing blocks within them.

That sounds like an argument against declaration expressions as well. Parenthesis don't make them clearer than curly braces. If we're going to champion a language change that allows for multiple statement expressions in any form I argue that it should at least try to build on existing syntax for multiple statements, aka blocks. Lots of languages allow blocks to return a value. Heck, expression trees in the BCL do too. And I don't think I've met a language aside C# that didn't allow multiple statements to comprise the expression in a match arm.

Thaina commented 5 years ago

@DavidArno As the HaloFour was stated

This syntax

-> {
    var circle = (Circle) shape;
    var radius = c.getDiameter() / 2;
    yield Math.PI * radius * radius;
}

Can be rewritten into declaration expression

: (
    var circle = (Circle) shape;
    var radius = c.getDiameter() / 2;
    Math.PI * radius * radius
)

And it totally horrible in my opinion. It not look like C# code at all. Seemingly like a javascript hacky code

alrz commented 5 years ago
    case RECTANGLE -> {
        var rect = (Rectangle) shape;
        yield rect.getWidth() * rect.getHeight();
    };

@HaloFour I suppose yield exits the control, unlike yield return,

=> {
  foreach (var item in list)
    yield item;
  yield default;
}

I'm not saying that it could be confused, actually it makes the perfect sense.

Since sequence expressions only have a single exit point, they would be very limited compared to this.

(btw this discussion belongs to https://github.com/dotnet/csharplang/issues/377)

333fred commented 4 years ago

I've made a new issue for my proposal to enhance switch statements, which supersedes this proposal: https://github.com/dotnet/csharplang/issues/3038.

KylePreuss commented 3 years ago

I realize this hasn't seen updates in quite some time but I haven't seen this issue's titular feature request specifically called out in the other linked issues/proposals. Will we ever be able to execute expression statements using the switch pattern matching syntax when we're not trying to make a separate method that returns something? e.x. will users ever be able to compile this:

public void DoSomeStuff( int x )
{
  DoThings();

  x switch
  {
    5 => Console.WriteLine( "x is 5!" ),
    10 => Console.WriteLine( "x is 10!"),
    _ => Console.WriteLine( "Just printing something, since apparently I can't have an empty catch-all / discard arm." )
  };

  // Now do some more stuff. Hopefully the above switch pattern doesn't cause our method to return. That feels strange.
  DoThings();
}
333fred commented 3 years ago

A switch statement works just fine for that case @KylePreuss.

KylePreuss commented 3 years ago

@333fred This does not compile for me, instead I receive the following error: CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement

EDIT: Example here - https://dotnetfiddle.net/Fc6Euo

spydacarnage commented 3 years ago

@KylePreuss 333fred was suggesting that you can just revert back to the older switch statement, not expression:

public void DoSomeStuff( int x )
{
  DoThings();

  switch (x)
  {
    case 5: 
        Console.WriteLine( "x is 5!" );
        break;
    case 10: 
        Console.WriteLine( "x is 10!");
        break;
    default: 
        Console.WriteLine( "No longer need this line but still worth having." );
        break;
  };

  // Now do some more stuff. Hopefully the above switch pattern doesn't cause our method to return. That feels strange.
  DoThings();
}
neon-sunset commented 2 years ago

It is very often I would like to do something like num switch { 1 => DoOne(), 2 => DoTwo(), _ => DoDefault } similar to how one would do so in rust match. This feature is a must!

Pilchard123 commented 2 years ago

I'm throwing my hat in the ring here too; it is quite often that I want to branch based on some tuple or the like, but not return any value. I could use an old-style switch statement, but that comes with no guarantees of being exhaustive where a switch expression often does. As a example (somewhat changed to protect the innocent):

(messagingServiceEnabled, attemptSendMessage, numberToSend) switch
{
  (true, true, <= 3) => await SendMessagesNow(),
  (true, true, _ ) => QueueManyMessages(),
  (false,true, _) => throw new InvalidOperationException("cannot send messages when messaging service not enabled"),
  (_, _, _) => /* do nothing; there would perhaps have to be some new syntax here for a no-op branch. Maybe void? */
};

I might not want to use every branch (as in the final case there), but I would like to be able to know that I've at least considered every branch. A no-op branch should also not be required to be the all-discard branch either. It should be possible to write something like

(behaviourEnum, timesToRepeat) switch
{
  (_, 0) => /* do nothing */,
  (Behaviour.Print, _) => PrintMessage(timesToRepeat),
  (Behaviour.TTS, _) => SpeakMessage(timesToRepeat),
  (Behaviour.NoOp, _) => /* do nothing */,
  (_, _) => throw new ArgumentOutOfRangeException()
};
BreyerW commented 2 years ago

@Pilchard123 returning discard seems fitting for noop branch like (Behaviour.NoOp)=>_, which would read as discard this branch

Pilchard123 commented 2 years ago

Yeah, I'd be on board for that. I don't have any strong opinions on what the "this branch is a no-op" syntax should be, at least not yet. Maybe someone will come up with something particularly beautiful or hideous and that'll change, but _ seems as good an option as any. Whould it run into trouble if it had been used as an identifier elsewhere in scope?

BreyerW commented 2 years ago

@Pilchard123 if _ is indentifier in scope then switch will try to find best fitting return type and error (since all other branches would be void or very likely not matching) but this is barely issue since switch expressions as statements would be new feature and discard-as-indentifier already can mess up upgrading legacy code but in practice its pretty rare to be an actual problem afaik

CyrusNajmabadi commented 2 years ago

@ccarlo88 Your post is abusive and against the code of conduct of the .Net foundation. Please find a way to express your thoughts in a constructive and welcoming fashion.

ccarlo88 commented 2 years ago

@CyrusNajmabadi Ok, removed my comment. I will try to agree with everything said next time. Maybe this is the reason everyone hates C# today.

CyrusNajmabadi commented 2 years ago

@ccarlo88 The sarcasm and abusive comments are not welcome. Please read the code of conduct here: https://dotnetfoundation.org/about/code-of-conduct

Especially these parts:

There are tons of people who visit here who have differing opinions on things. Nearly all of them are able to constructively share those ideas and discuss them with others. If you cannot, please find some other place to express your negativity.

ccarlo88 commented 2 years ago

@CyrusNajmabadi sorry for anyone who may be affected. I am just a guy (from many) who got tired since dotnet became a "foundation".

CyrusNajmabadi commented 2 years ago

@ccarlo88 That's fine. Just keep the tone civil from now on, and there won't be a problem. Thanks! :)

Crauzer commented 1 year ago

Any update on this ? It would be great to have this instead of the ugly switch with breaks or regular branching.

ccarlo88 commented 1 year ago

It would be great if people stop creating new syntax in C#. Some reasons to not implement that:

  1. If it is a 2 element you can use a ternary operator: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/conditional-operator

  2. If there's more I don't expect you to put everything in one line. So, you have the standard switch or the new switch expression: https://learn.microsoft.com/en-US/dotnet/csharp/language-reference/operators/switch-expression

  3. Changing 2 variables in one line was hard for me to even notice that you were changing on true 's' but on false 'x'

  4. C# is supposed to be the evolution of C++ but is becoming a weird mixture of other languages. If people keep changing it, it gets hard for people to read each other's code.

HaloFour commented 1 year ago

@ccarlo88

C# is supposed to be the evolution of C++ but is becoming a weird mixture of other languages. If people keep changing it, it gets hard for people to read each other's code.

Yet C++ continues to evolve and gain new features and syntax, often borrowed from other languages. For example, C++20 has co_await, to be used in asynchronous coroutines, which was clearly borrowed from C#.

As long as there are friction points in the language worth addressing the language will continue to evolve. The switch statement, as it was inherited from C++, can certainly be considered a wart that has a weird syntax and behavior compared to contemporary languages.

ccarlo88 commented 1 year ago

@HaloFour You pointed 2 different points, syntax and features. Please, correct me if I am wrong but co_await is regarding feature and the issue here is regarding syntax. Feature is always welcome but creating different syntax is an issue. Not even Google, who is a complete disappointment regarding programming syntax, keeps including "alternative" syntax.

HaloFour commented 1 year ago

@ccarlo88

Feature is always welcome but creating different syntax is an issue.

I don't see much of a distinction between the two. If the existing syntax creates friction in common use cases then there is nothing wrong with introducing alternative syntax to improve the developer ergonomics and to create a pit of success. It's not all that common that a new syntax maps to completely new features that didn't already have possible solutions.

Not even Google, who is a complete disappointment regarding programming syntax, keeps including "alternative" syntax.

I've yet to meet a programming language that hasn't adopted additional syntax to smooth over friction in existing features. The switch statement, as inherited from C++, is very awkward to use. Ternary operators are fine for chained boolean conditionals, but switch, given it's integration with pattern matching, is capable of a lot more.

I'm sure that there is plenty of syntax that has been added to C# over the past 22 years that you would find challenging to live without today, even if there are ways to accomplish the same exact features in C# 1.0. I'm sure that there are places in the language where you think things could be made more concise or more declarative in order to simplify tasks you commonly perform. Everyone has a different subset of features they use or want to see added.

ccarlo88 commented 1 year ago

@HaloFour friction in a switch? Again, C++ -> C#, evolution. Developers waste time in this kind of issue instead of focusing on more useful things (maybe people are more worried in fitting things in a sprint LOL)

In fact, giving my opinion, people who doesn't like how C# syntax is, could find an alternative programming language. C# syntax is probably good because a very smart CEO (Ballmer) didn't allow some people to give corporate decisions. There are ways to make C# easier, but is not removing classes or changing switch syntax.

Take a look at Go or Python, they are a joke! Python is a joke by itself, if you have 10 lines of code you have 10 bugs. Go, well, I don't believe that someone reviewed that syntax at Google.

There's this mentality which we must fulfill everyone's expectation, which then leads to a bad solution for 98% but makes 2% happy. Oh, this remembers me, isn't this the story of Xamarin at Microsoft?