dotnet / csharplang

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

Proposal: allow comparison operators in switch case labels C# (VS 16.8, .NET 5) #812

Open gafter opened 7 years ago

gafter commented 7 years ago

@JFMous commented on Sat May 14 2016

It would be nice if the case labels in switch statement supported comparison operators, much like the Select Case statement in Visual Basic does.

Now, there are only two options for the switch labels:

switch-label: case constant-expression : default :

This could be extended to:

case [< | > | <= | >=] constant-expression [ to ] constant-expression : default :

So you could write code as in this example:

int iq = DoIqTest();

switch (iq)
{
    case <= 69:
        ProcedureExtremelyLow();
        break;
    case 70 to 79:
        BorderlineProcedure();
        break;
    case 80 to 89:
        LowAverageProcedure();
        break;
    case 90 to 99:
    case 101 to 109:
        AverageProcedure();
        break;
    case 100:
        ExactlyMedianProcedure();
        break;
    case 110 to 119:
        HighAverageProcedure();
        break;
    case 120 to 129:
         SuperiorProcedure();
         break;
    case >= 130:
        VerySuperiorProcedure();
        break;
}

The 'to' keyword would be used to specify a range. In the statement switch (value), case x to y: would be equivalent to the boolean expression value >= x && value <= y


@HaloFour commented on Sat May 14 2016

Case guards in pattern matching #206 will open this up to allowing for arbitrary conditions:

int iq = DoIqTest();

switch (iq)
{
    case * when iq <= 69:
        ProcedureExtremelyLow();
        break;
    case * when iq <= 79:
        BorderlineProcedure();
        break;
    case * when iq <= 89
        LowAverageProcedure();
        break;
    case * when iq == 100:
        ExactlyMedianProcedure();
        break;
    case * when iq <= 109:
        AverageProcedure();
        break;
    case * when iq <= 119:
        HighAverageProcedure();
        break;
    case * when iq <= 129:
         SuperiorProcedure();
         break;
    default:
        VerySuperiorProcedure();
        break;
}

The wildcard pattern would match on anything but then the case guard would contain the specific conditions. They would be evaluated in lexical order.


@alrz commented on Sun May 15 2016

would it make sense to make case optional or perhaps switch expression?

switch {
  when id <= 69: ... break;
  ...
}

might be preferrable over if else.


@bondsbw commented on Mon May 16 2016

@alrz I like that somewhat, feels like a reasonable simplification.

But I feel this may be even more useful in pattern matching. In match expressions, the roughly equivalent form might be:

var x = match
{
    when iq <= 69: ProcedureExtremelyLow(),
    when iq <= 79: BorderlineProcedure(),
    when iq <= 89: LowAverageProcedure(),
    when iq <= 99 || (iq <= 109 && iq >= 101): AverageProcedure(),
    when iq == 100: ExactlyMedianProcedure(),
    when iq <= 119: HighAverageProcedure(),
    when iq <= 129: SuperiorProcedure(),
    default: VerySuperiorProcedure()
};

I like the possibilities but if iq is always the comparison, it really feels like it should be the subject of the match expression. Something like the following feels more like pattern matching (although it would require a bit of rethinking what a relational expression is):

var x = iq match
{
    when <= 69: ProcedureExtremelyLow(),
    when <= 79: BorderlineProcedure(),
    when <= 89: LowAverageProcedure(),
    when == 100: ExactlyMedianProcedure(),
    when <= 109: AverageProcedure(),
    when <= 119: HighAverageProcedure(),
    when <= 129: SuperiorProcedure(),
    default: VerySuperiorProcedure()
};

And I'm not sure I like changing

when iq <= 99 || (iq <= 109 && iq >= 101):

into something like

when <= 99 || (<= 109 && >= 101):

@Unknown6656 commented on Mon May 23 2016

@bondsbw, @JFMous : I would like to see something like the following implemented with the match-pattern:

Func<int, string> func1, func2, func3, ...;

int iq = ...;
string result = iq match
{
    when <= 100: func1,
    when == 100: func2,
    when >= 100: func3,
    // ....
    default: funcx()
}(iq);

Meaning, the possibility to use the match-statement inside of expressions.


@HaloFour commented on Mon May 23 2016

@Unknown6656 The proposal is that match is an expression, so of course you'd be able to use it within other expressions. You can think of it like a ternary op on steroids.

As for your specific example, there have been no proposed range patterns. Nor has there been a proposal to allow for only case guards with an implied wildcard pattern. So as of now your example would be:

string result = iq match (
    case * when iq < 100: func1,
    case * when iq == 100: func2,
    case * when iq > 100: func3,
    case *: funcx
)(iq);
DavidArno commented 7 years ago

Rather than implementing this limited feature, could the team just finish implementing pattern matching, including match expressions and get that released, please?

bondsbw commented 7 years ago

@DavidArno Haha there is a thing called "priorities".

DavidArno commented 7 years ago

@bondsbw,

Exactly. They should prioritise finishing the half-complete pattern matching feature over adding new features like nullable reference types and those interface thingies 😉

iam3yal commented 7 years ago

@DavidArno I agree that match expression would be awesome to have sooner than later but don't know if this is the place to discuss priorities, it's a reasonable suggestion, at least in my opinion.

p.s. I wouldn't downvote something just due to priorities, not saying you did but just a side note. 😉

bondsbw commented 7 years ago

I feel like these partial infix expressions could be a more general feature.

iqs.Where(x => x < 100)

could be simplified to

iqs.Where(< 100)
iam3yal commented 7 years ago

@bondsbw Well, if you think that this feature should be generalized then I think that it warrants a new post.

bondsbw commented 7 years ago

I'll start a new post if there is interest.

qaqz111 commented 7 years ago

The pattern match already can handle this for now:

            var a = 16;
            switch (a)
            {
                case int _ when a < 100:
                    Console.WriteLine(a);
                    break;
                case int _ when a < 200:
                    Console.WriteLine(a + a);
                    break;
            }

With when you can do a more complex play:

            var a = 16;
            switch (a)
            {
                case int _ when a < 100:
                    Console.WriteLine(a);
                    break;
                case int _ when IsOK(a):
                    Console.WriteLine(a + a);
                    break;
            }

        private bool IsOK(int a) => true; //<- replace with your own logic

The weak point is, you can not pass a long to the switch, and the when clause is indeed a bit long.

srburton commented 6 years ago

Very good proposal

gafter commented 6 years ago

I'm championing this as the idea of having comparison-based pattern-matching forms.

HaloFour commented 6 years ago

@gafter

Neat. Ideas around syntax?

if (person is Student { Gpa: > 3.0 }) { ... }
gafter commented 6 years ago

@HaloFour Yes, that syntax is fine. Also case > 10. Not sure how it should work when the input is of type object, for example.

bondsbw commented 6 years ago

I'm curious if that syntax can be generalized into partial application.

gafter commented 6 years ago

@bondsbw Can you give an example? Remember these are patterns, not expressions.

For ranges, I expect the syntax would be something like in 1 to 10, for example

case in 1 to 10: if (x is in 1 to 10)

etc.

bondsbw commented 6 years ago

@gafter This would be outside of pattern matching, outside of the scope of the current proposal but related.

The idea would be to open up expressions like I mentioned above:

iqs.Where(< 100)

Say we have a binary operator x op y with the function signature Tx * Ty -> Tresult. Then x op would be a partial application with function signature Ty -> Tresult, and op y would be a partial application with function signature Tx -> Tresult.

To tie it all together, partial applications could be substituted for method groups with a compatible signature. < 100 has the signature int -> bool and is compatible with the predicate parameter of Where.

bondsbw commented 6 years ago

A different idea... allow patterns to be substituted for any method group with signature T -> bool.

Then we could have

iqs.Where(is < 100)

but also the much broader range of patterns:

iqs.Where(is in 120 to 140)

people.Where(is Student { Gpa: > 3.0 })
svick commented 6 years ago

@bondsbw How would your first idea work with operators that are both binary and unary, like +, - or *?

In other words, iqs.Select(- 100) is already valid syntax (which may or may not actually compile, depending on overload resolution), so I don't think you could change its meaning.

bondsbw commented 6 years ago

@svick + and - are the only ones that could be ambiguous (I think... please correct me). In those cases it would have to prefer to interpret it as the unary operator, for the sake of BC.

I think I prefer the pattern approach, though it is limited to situations where the return value is bool.

theunrepentantgeek commented 6 years ago

Any operator with both a binary and unary form could trigger this ambiguity - which includes *, & and | as well.

Building on the pattern matching syntax we already have, the when syntax opens up every possible predicate in an extremely readable manner.

Mattias-Viklund commented 5 years ago

Id like to propose an extension to this.

Where you can do something like this:

int a = 3;
int b = 2;
int c = 1;

switch (a) {
    case ((& c) == c):
        Console.WriteLine("(a & c) == c");
        break;
    case (>> 1 == c):
        Console.WriteLine("(a >> 1) == c");
        break;

    case (-b == c):
        Console.WriteLine("(a - b) == c");
        break;

}

Might be silly though. (Sorry, not sure on how to format code in a comment)

Unknown6656 commented 5 years ago

@Mattias-Viklund your last switch case is a bit flawed, as it would be confusing whether you meant a-b == c or -b == c (both are currently valid expressions).

I would rather see a Haskell-like currying and partial application of operators, however, this would require currying in general to be introduced to C#. This would allow expressions such as:

Func<int, int> = (>>2);

...which could be applied to your example above in a more general manner.

Mattias-Viklund commented 5 years ago

Oh yeah of course, dumb oversight on my part. but yeah I would want something to add to switches in order to make them more flexible.

zspitz commented 5 years ago

Would this also work with strings, as in VB.NET?

var s = GetArbitraryString();
switch (s)
    case >= "n":
        Console.WriteLine("Past the midpoint");
        break;
    case "m" to "n":
        Console.WriteLine("Starts with 'm');
        break;
}
Unknown6656 commented 5 years ago

@zspitz : Strings would have to define comparison operators like they do in VB.NET. So, as long as they are not defined, your code wouldn't work.

However, you could currently "solve" your code sample by replacing strings with chars:

string s = ........;

switch (s[0])
{
    case >= 'n':
        Console.WriteLine("Past the midpoint");
        break;
    case 'm' to 'n':
        Console.WriteLine("Starts with 'm'");
        break;
}
Thaina commented 5 years ago

I would like to vote against the to syntax. It still very ambiguous

And I would favor and and or syntax in pattern instead

So 1 to 10 would just become >= 1 and <= 10 or > 0 and < 11

gafter commented 5 years ago

The LDM confirmed this as part of a set of pattern-matching features for consideration in C# 9.0. This depends for its utility on the and pattern combinator, so we would not do it before that.