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

What are the semantics of 'goto case' in the presence of pattern-matching? #8821

Closed
gafter opened this issue Feb 17, 2016 · 27 comments
Closed

Comments

@gafter
Copy link
Member

gafter commented Feb 17, 2016

We need a language specification for the intended behavior of goto case when the switch contains pattern-matching constructs. And then we need to implement that.

@DavidArno
Copy link

Don't allow goto case in pattern matches, as they make no sense. This is a good example of why re-using switch syntax for "unit" (void) pattern matches is a bad idea.

@gafter
Copy link
Member Author

gafter commented Feb 18, 2016

@DavidArno The ordinary switch statement is a pattern match, and we already allow goto case in them, so that won't work.

@DavidArno
Copy link

The ordinary switch statement is a pattern match

Is that really true? Surely the current switch is a constant-value matcher, not an expression matcher? Currently, with switch, the order of the cases only matters if fall-through of empty cases is used. Further, as a case must end with a break, return or goto, the last can be used to replicate C-like fall-through after a case statement.

With expressions though, everything changes. Now, the order is very important, as each expression must be tested in that precise order and both fall-through and goto do not really make any sense.

My interpretation of your thoughts in #4944 was that the current switch and pattern matching statements are so different, we needed a new match keyword. Now you appear to be arguing that we can somehow shoe-horn pattern matching into the current switch, keeping all the current functionality, plus adding pattern matching, all without breaking existing code. Maybe this is possible, but it strikes me we are in danger of disappearing down a rabbit hole of confusing functionality if we do not make the two (current and pattern-match switches) quite distinct things.

@gafter
Copy link
Member Author

gafter commented Feb 18, 2016

Is that really true? Surely the current switch is a constant-value matcher, not an expression matcher? Currently, with switch, the order of the cases only matters if fall-through of empty cases is used. Further, as a case must end with a break, return or goto, the last can be used to replicate C-like fall-through after a case statement.

Yes, it is really true. Our intent is that the extension of switch to support pattern-matching is an upward compatible pure extension.

With expressions though, everything changes. Now, the order is very important, as each expression must be tested in that precise order and both fall-through and goto do not really make any sense.

Patterns are not general expressions, and the specification does not say what order the matching occurs - in fact, it is explicitly unspecified. It only says that the first case that matches is the one whose body is executed.

it strikes me we are in danger of disappearing down a rabbit hole of confusing functionality if we do not make the two (current and pattern-match switches) quite distinct things.

I haven't yet seen any examples where confusing things would happen. Can you think of any?

@gafter gafter modified the milestone: 2.0 (RC) Feb 18, 2016
@DavidArno
Copy link

The following code example tries to capture some of the things that could be confusing if switch is simply extended to support patterns on top of existing functionality:

switch (shape) 
{
    case Line:
    case Square(var length) when length < 5.0:   // fall-through would be allowed here?
        Action1();
        break;
    case Square(var length):  // should the compiler emit a warning that length isn't used?
    default:                  // should the compiler emit a warning that default isn't the 
                              // last case?
        Action2();
        break;
    case Circle(var radius):        // should the compiler give an error here that we are 
                                    // mixing vars?
    case Ellipse(var r1, var r2):   // or here?
        Action3(radius);            // or here, as radius may not exist in this context?
        break;
    case Rectangle:
        Action4();
        goto case Square;   // and the topic of this thread: how do I use goto with 
                            // patterns? Really bad case would be 
                            // goto case Square(var length) when length < 5.0, 
                            // does that goto the second case or to 
                            // case Square(var length) 
                            // only when length < 5, ie does it perform the check before the 
                            // goto? What happens if the check fails?
}

Next, whilst it will now be possible to switch on non-constants, the syntax will be really weird. I couldn't do:

var a = new SomeType();
var b = new SomeType();
var c = new SomeType();
var d = a;

switch (d)
{
    case a:
        Action1();
        break;
    case b:
        Action2();
        break;
    case c:
        Action3();
        break;
    default:
        Action4();
        break;
}

Instead, it would need to be:

var a = new SomeType();
var b = new SomeType();
var c = new SomeType();
var d = a;

switch (d)
{
    case var x where x == a:
        Action1();
        break;
    case var x where x == b:
        Action2();
        break;
    case var x where x == c:
        Action3();
        break;
    default:
        Action4();
        break;
}

In the above case, it's clear why the syntax is like that, but in more complicated situations, it could be very unintuitive for many as to why they need to switch from one syntax to another when writing a switch statement.

None of the above issues are insurmountable. However, I feel they highlight some of the ways in which just extending switch to both be backward compatible and support patterns could create a lot of confusion. With reference to @HaloFour's question on #206:

Is the goal to extend the existing switch syntax with the ability to use pattern matching, or is the goal to provide a statement-based form of pattern matching?

You seem very much in favour of the former. To my mind at least, this is a mistake as it opens up a whole serious of potential areas of confusion. If we go down the route of providing a statement-based form of pattern matching, it is clearly delineated into existing functionality with one syntax and a whole new type of functionality with another. Writing:

match using (d)
{
    case var x where x == a: Action1();
    case var x where x == b: Action2();
    case var x where x == c: Action3();
    default: Action4();
}

creates something syntactically distinct. It's obvious to the developer that they are using something very different to switch and the areas of confusion and issues around goto case and fall-through just disappear.

(I've used match using above as I think that creates a solution to avoiding match being a breaking change when used in statements).

@alrz
Copy link
Member

alrz commented Feb 19, 2016

@DavidArno You don't goto patterns, you goto with an expression and the switch evaluates it in order from the top.

In the first case, length is not definitely assigned in the case body but I presume it's ok in the when.

In the second case length is not definitely assigned in the case body but since you didn't use it you'd get a warning (and if you use it you'd get an error, the only place you could use it is in the when clause).

Per @gafter's comment:

I would be fine adding a warning (or perhaps even an error) when a default appears anywhere but the last position in a switch that contains any pattern-matching syntax.

But I think it doesn't need to, because in case of any misuse, variables become definitely not assigned and you will know that something's wrong (but it must be handled last — it's not a synonym for case *).

In any language that feature pattern-matching there is no dedicated switch statement for pattern-matching and regular switch, because basically the former is a superset of the latter. But they do have two forms for expression and statement contexts (if they are not expression-based).

@DavidArno
Copy link

@alrz,

You don't goto patterns, you goto with an expression and the switch evaluates it in order from the top.

I'm assuming that - given the existence of this issue - your assertion is far from certain.

In any language that feature pattern-matching there is no dedicated switch statement for pattern-matching and regular switch, because basically the former is a superset of the latter. But they do have two forms for expression and statement contexts (if they are not expression-based).

Do you know of any other language that has added pattern matching after defining switch in a previous version? F# for example, uses the same syntax for expressions and statements as the latter is treated as a unit expression. This isn't something that C# could do though, so we need the two forms.

My main motivator for wanting pattern statements to not use switch is because the latter's syntax is so verbose. I'm firmly in the "never use switch; use a Dictionary of Action instead" camp. The code tends to be shorter, simpler and avoids the real-world switch usage of tens or hundreds of lines of code in each case. So I'm driven very much by wanting pattern matching statements to avoid switch as I don't want to have to start using that bloated syntax when pattern matching. My examples though are real concerns over both the mess of double-purpose syntax I'd have to handle and the confusion it'll cause for those still using switch when they are exposed to the world of patterns.

@alrz
Copy link
Member

alrz commented Feb 19, 2016

It's not an "assumption". See #7703,

never use switch; use a Dictionary of Action instead" camp

You gotta check out TypeSwitch in the Roslyn that specifically is a workaround for pattern-matching, I don't see why it's better. Not to mention that switch doesn't create a delegate for each case, who wants that?

double-purpose syntax

It's not! because constants are basically a constant-pattern so you'd have the same behavior with an extended switch construct. It's not something else.

My main motivator for wanting pattern statements to not use switch is because the latter's syntax is so verbose

The only difference that I see in your proposed syntax is the removed break which is explicitly mandatory to prevent accidental mess-up.

For other points you've made I woudn't repeat my answer.

@HaloFour
Copy link

@DavidArno

I'm assuming that - given the existence of this issue - your assertion is far from certain.

While probable this isn't the first time it's come up: #7703

And that does make sense as you can interpret goto case to specify the value today in switch since the value and the label are one in the same.

I like the idea of having pattern matching permeate the language, and adding support to existing language constructs like if, is and switch seems to be a good way to accomplish that. Very specifically I see utility patterns being used to augment existing switch statements with range comparisons and the like, a feature requested numerous times (one VB.NET already supports). That said I agree that switch was crufty before it was even adopted in C# and I would not be opposed to a cleaner statement form of pattern matching on top of switch. Although if switch scratches the itch I guess that would become unlikely.

@alrz
Copy link
Member

alrz commented Feb 19, 2016

@HaloFour What is wrong with switch already? And what syntax you'd propose to address it? Removing break perhaps?

@HaloFour
Copy link

@alrz My biggest beef would probably be the lack of scoping between the cases. C# did at least correctly identify and prevent accidental fall-through which was it's biggest pit of failure. But because of that the requirement of break is just legacy noise, required to maintain some semblance of compatibility with much older languages but otherwise offering no value.

I propose nothing to address these concerns since that ship has already sailed.

@DavidArno
Copy link

@alrz,

The only difference that I see in your proposed syntax is the removed break which is explicitly mandatory to prevent accidental mess-up.

Given the scenario where every case is followed by a method call and a break (which is how I'd use switch to pattern match), getting rid of break let's me put the method call on the same line as the case in a readable way and achieve a 300% decrease in the size of the statement. That is a massive reduction and a compelling argument for not using switch in itself.

@alrz
Copy link
Member

alrz commented Feb 19, 2016

@HaloFour While I agree that lack of scoping between the cases is not helping you're free to use blocks to define separate scopes. But I believe all this is because C# begins with a C. And if you're gonna argue with that you'd better off with another language.

@DavidArno You can also put the break in the same line following the method call. See #1426 for discussion regarding removing break.

All in all, inventing yet another syntax on top of this with the exact same semantics to solve the exact same problem seems like this.

@HaloFour
Copy link

@alrz Sure, you can use blocks. But switch is the only place in C where you can either use multiple statements or a block, which makes it inconsistent with itself. Of course that is C#'s legacy and I don't fault them for taking the syntax largely as it was. But I have to believe that, hindsight being 20/20, even the designers of C would have thought differently about switch if they could do it all over again.

@alrz
Copy link
Member

alrz commented Feb 19, 2016

@HaloFour The Legacy made it to 2017. If that was absurdly a mistake they should have abandoned it a long time ago. My thinking is that switch isn't the problem, C# is. But for some reason (the legacy) MS decided to embrace it instead of inventing another language. C designers didn't think of that either, so there is no one to blame. There is a proverb that says خشت اول چون نهد معمار کج تا ثریا میرود دیوار کج, but it's past and done. I'm arguing with you on "I would not be opposed to a cleaner statement form of pattern matching on top of switch". Do you think that it really helps? switch is not the only problem, if you're willing to change everything, imagine what a sweet language C# would become.

@DavidArno
Copy link

@alrz,

I get your point re the XKCD comic and it's one well made. In reality though, we have Dictionary<SomeEnum, Action<T>> , which is an alternative to switch, which is an alternative to if. We have lamdas, which are an alternative to named methods. We have LINQ query syntax , which is an alternative to method chaining etc etc. The language is full of multiple ways of doing the same thing. What harm, one more?

The scenario as I see it would be match using being offered up as modern, tidier way of achieving what switch does, with all the glorious beauty of patetrn matching to boot. Then either ReSharper or some Roslyn Analyzer offers a way of automatically rewriting switch statements. switch effectively becomes a "good practice" deprecated feature, the new match using statement has more readable, succinct syntax. Everyone's then happy. Hey, I can dream... :)

@alrz
Copy link
Member

alrz commented Feb 19, 2016

@DavidArno As I said, switch doesn't need a delegate per case. Dictionary<SomeEnum, Action<T>> would be a solution only when cases are not known at compile time. switch-like statements like what you have suggested has been proposed before (#1638).

@DavidArno
Copy link

@alrz,

I don't follow your "switch doesn't need a delegate per case". Not does a dictionary. It has one delegate (typically Action<...> or Func<...>). It needs either a named method or lambda per case though, which would also apply to well-written switch statements.

A real-world example, would be:

    public sealed class Union<T1, T2, T3, T4>
    {
        private readonly Dictionary<Variant, Func<int>> _hashCodes;
        ...

        private Union()
        {
            _hashCodes = new Dictionary<Variant, Func<int>>
            {
                {Variant.Case1, () => _value1.GetHashCode()},
                {Variant.Case2, () => _value2.GetHashCode()}
                {Variant.Case3, () => _value3.GetHashCode()}
                {Variant.Case4, () => _value4.GetHashCode()}
            };
            ...
        }

        ...

        public override int GetHashCode() => _hashCodes[Case]();
    }

Obviously with C# 7, I'll be able to rewrite that as:

        public override int GetHashCode() => Case match(
            case Variant.Case1: _value1.GetHashCode()
            case Variant.Case2: _value2.GetHashCode()
            case Variant.Case3: _value3.GetHashCode()
            case Variant.Case4: _value4.GetHashCode()
        );

which will be nice. The point is though, dictionaries can already be used to replace if and switch and - as a study of questions and answers around switch on StackOverflow would show - a growing number of people are likewise doing this, not just me.

@HaloFour
Copy link

Some people don't like the slightly more bloated syntax so they opt for the significantly more bloated execution? This is why I generally avoid stackoverflow.

@alrz
Copy link
Member

alrz commented Feb 19, 2016

It has one delegate, but multiple instances per each case, don't get me started on captured variables etc. Please don't.

@gafter
Copy link
Member Author

gafter commented Feb 19, 2016

@DavidArno

Given the scenario where every case is followed by a method call and a break (which is how I'd use switch to pattern match)

You don't want to use a switch statement. You want to use a match expression.

@DavidArno
Copy link

DavidArno commented Feb 19, 2016

@gafter,

Will that work even if the statement for each case is a void, I've like F#'s unit expressions?

@alrz
Copy link
Member

alrz commented Feb 21, 2016

That is weird, because accouring to Eric Lippert,

There is only one kind of expression in C# which does not produce some sort of value, namely, an invocation of something that is typed as returning void.

So this is allowed:

void F() => G();

And since this would be allowed:

void F(T e) => e match(case P : G());

It'd be unfortunate that this woudn't be allowed:

{
  e match(case P : G());
}

I might really prefer curly braces and allow this:

{
  e match {
    case P : G()
  }
}

perhaps, without a semicolon at the end.

@gafter
Copy link
Member Author

gafter commented Feb 21, 2016

I do not expect to allow a match expression to be of type void.

And since this would be allowed:

void F(T e) => e match(case P : G());

Unlikely if G returns void.

@alrz
Copy link
Member

alrz commented Feb 22, 2016

So, to my understanding it's an expression in the same sense as a ternary,

void G() { .. }
void F() => cond ? G() : G();

Because this also woudn't work if either F or G are void returning. Makes sense.

@DavidArno
Copy link

@grafter,

The two statements,

You don't want to use a switch statement. You want to use a match expression.

and

I do not expect to allow a match expression to be of type void.

are at odds with each other. Under the proposed spec, I can't do the following:

switch (d)
{
    case var x where x == a: Action1();
    case var x where x == b: Action2();
    case var x where x == c: Action3();
    default: Action4();
}

If I'm dealing with Func<>, life is tidy:

var x = match (d)
{
    case var x where x == a: Func1();
    case var x where x == b: Func2();
    case var x where x == c: Func3();
    default: Func4();
}

Whereas, if I'm dealing with void methods, it has to be either the long-winded form:

switch (d)
{
    case var x where x == a: 
        Action1();
        break;
    case var x where x == b: 
        Action2();
        break;
    case var x where x == c: 
        Action3();
        break;
    default: 
        Action4();
        break;
}

Or the hard to read:

switch (d)
{
    case var x where x == a: Action1(); break;
    case var x where x == b: Action2(); break;
    case var x where x == c: Action3(); break;
    default: Action4(); break;
}

@gafter
Copy link
Member Author

gafter commented Jun 26, 2016

We have already implemented the intended semantics in the master branch, which are that you can use goto case 1 and it matches case 1:, even if some earlier case would have caught an input value of 1 in the switch statement.

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

4 participants