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

Champion: Switch expression as a statement expression #2632

Open
gafter opened this issue Jul 4, 2019 · 30 comments
Open

Champion: Switch expression as a statement expression #2632

gafter opened this issue Jul 4, 2019 · 30 comments

Comments

@gafter
Copy link
Member

@gafter gafter commented Jul 4, 2019

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 };
}
@gafter gafter self-assigned this Jul 4, 2019
@gafter gafter added this to TRIAGE NEEDED in Language Version Planning Jul 4, 2019
@DavidArno

This comment has been minimized.

Copy link

@DavidArno DavidArno commented Jul 4, 2019

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

This comment has been minimized.

Copy link

@Mteheran Mteheran commented Jul 5, 2019

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

This comment has been minimized.

Copy link

@dsaf dsaf commented Jul 6, 2019

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

@CyrusNajmabadi

This comment has been minimized.

Copy link

@CyrusNajmabadi CyrusNajmabadi commented Jul 6, 2019

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

@dsaf

This comment has been minimized.

Copy link

@dsaf dsaf commented Jul 6, 2019

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

@alrz

This comment has been minimized.

Copy link
Contributor

@alrz alrz commented Jul 7, 2019

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

This comment has been minimized.

Copy link

@CyrusNajmabadi CyrusNajmabadi commented Jul 7, 2019

@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

This comment has been minimized.

Copy link

@Thaina Thaina commented Jul 7, 2019

What happen to

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

?

@DavidArno

This comment has been minimized.

Copy link

@DavidArno DavidArno commented Jul 8, 2019

@Thaina,

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

@Thaina

This comment has been minimized.

Copy link

@Thaina Thaina commented Jul 8, 2019

@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

This comment has been minimized.

Copy link
Contributor

@yaakov-h yaakov-h commented Jul 8, 2019

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

This comment has been minimized.

Copy link

@Thaina Thaina commented Jul 8, 2019

@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

This comment has been minimized.

Copy link

@DavidArno DavidArno commented Jul 8, 2019

@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

This comment has been minimized.

Copy link

@Thaina Thaina commented Jul 8, 2019

@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

This comment has been minimized.

Copy link

@DavidArno DavidArno commented Jul 8, 2019

@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

This comment has been minimized.

Copy link

@Thaina Thaina commented Jul 8, 2019

@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

This comment has been minimized.

Copy link

@DavidArno DavidArno commented Jul 8, 2019

@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

This comment has been minimized.

Copy link
Contributor

@yaakov-h yaakov-h commented Jul 8, 2019

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

Sharplab Playground.

@Thaina

This comment has been minimized.

Copy link

@Thaina Thaina commented Jul 9, 2019

@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

@MadsTorgersen MadsTorgersen moved this from TRIAGE NEEDED to 8.x Candidate in Language Version Planning Jul 17, 2019
@gafter gafter moved this from 8.x Candidate to 9.0 Candidate in Language Version Planning Aug 28, 2019
@gafter gafter added this to the 9.0 candidate milestone Aug 28, 2019
@gafter

This comment has been minimized.

Copy link
Member Author

@gafter gafter commented Aug 28, 2019

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

@ronnygunawan

This comment has been minimized.

Copy link

@ronnygunawan ronnygunawan commented Oct 4, 2019

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

This comment has been minimized.

Copy link

@DavidArno DavidArno commented Oct 4, 2019

@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

This comment has been minimized.

Copy link
Contributor

@HaloFour HaloFour commented Oct 4, 2019

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

This comment has been minimized.

Copy link

@DavidArno DavidArno commented Oct 4, 2019

@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

This comment has been minimized.

Copy link

@Thaina Thaina commented Oct 4, 2019

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

This comment has been minimized.

Copy link

@Thaina Thaina commented Oct 4, 2019

@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

This comment has been minimized.

Copy link
Contributor

@HaloFour HaloFour commented Oct 4, 2019

@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

This comment has been minimized.

Copy link

@Thaina Thaina commented Oct 4, 2019

@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

This comment has been minimized.

Copy link
Contributor

@alrz alrz commented Oct 5, 2019

    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 #377)

@333fred

This comment has been minimized.

Copy link
Member

@333fred 333fred commented Dec 19, 2019

I've made a new issue for my proposal to enhance switch statements, which supersedes this proposal: #3038.

@333fred 333fred removed their assignment Dec 19, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
You can’t perform that action at this time.