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

Proposed changes for Pattern Matching in C# 9.0 - Draft Specification #2850

Open
gafter opened this issue Oct 2, 2019 · 44 comments

Comments

@gafter
Copy link
Member

commented Oct 2, 2019

We are considering a small handful of enhancements to pattern-matching for C# 9.0 that have natural synergy and work well to address a number of common programming problems:

  • #1350 Parenthesized patterns to enforce or emphasize precedence of the new combinators
  • #1350 Conjunctive and patterns that require both of two different patterns to match;
  • #1350 Disjunctive or patterns that require either of two different patterns to match;
  • #1350 Negated not patterns that require a given pattern not to match; and
  • #812 Relational patterns that require the input value to be less than, less than or equal to, etc a given constant.

Parenthesized Patterns

Parenthesized patterns permit the programmer to put parentheses around any pattern. This is not so useful with the existing patterns in C# 8.0, however the new pattern combinators introduce a precedence that the programmer may want to override.

primary_pattern
    : parenthesized_pattern
    ;
parenthesized_pattern
    : '(' pattern ')'
    ;

Relational Patterns

Relational patterns permit the programmer to express that an input value must satisfy a relational constraint when compared to a constant value:

    public static LifeStage LifeStageAtAge(int age) => age switch
    {
        < 0 =>  LiftStage.Prenatal,
        < 2 =>  LifeStage.Infant,
        < 4 =>  LifeStage.Toddler,
        < 6 =>  LifeStage.EarlyChild,
        < 12 => LifeStage.MiddleChild,
        < 20 => LifeStage.Adolescent,
        < 40 => LifeStage.EarlyAdult,
        < 65 => LifeStage.MiddleAdult,
        _ =>    LifeStage.LateAdult,
    };

We imagine supporting ==, !=, <, <=, >, and >= patterns on all of the built-in types that support such binary relational operators with two operands of the same type in an expression. Specifically, we support all of these relational patterns for sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, and decimal. Additionally, we support == and != for string and bool. One could retcon the existing constant pattern as a special case of the == relational pattern.

primary_pattern
    : relational_pattern
    ;
relational_pattern
    : '<' expression
    | '<=' expression
    | '>' expression
    | '>=' expression
    | '==' expression
    | '!=' expression
    ;

The expression is required to evaluate to a constant value. It is an error if that constant value is double.NaN or float.NaN. It is an error if the expression is a null constant and the relational operator is <, <=, >, or >=.

When the input is a type for which a suitable built-in binary relational operator is defined that is applicable with the input as its left operand and the given constant as its right operand, the evaluation of that operator is taken as the meaning of the relational pattern. Otherwise we convert the input to the type of the expression using an explicit nullable or unboxing conversion. It is a compile-time error if no such conversion exists. The pattern is considered not to match if the conversion fails. If the conversion succeeds then the result of the pattern-matching operation is the result of evaluating the expression e OP v where e is the converted input, OP is the relational operator, and v is the constant expression.

Pattern Combinators

Pattern combinators permit matching both of two different patterns using and (this can be extended to any number of patterns by the repeated use of and), either of two different patterns using or (ditto), or the negation of a pattern using not.

I expect the most common use of a combinator will be the idiom

if (e is not null) ...

More readable than the current idiom e is object, this pattern clearly expresses that one is checking for a non-null value.

The and and or combinators will be useful for testing ranges of values

bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

This example illustrates our expectation that and will have a higher parsing priority (i.e. will bind more closely) than or. The programmer can use the parenthesized pattern to make the precedence explicit:

bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

Like all patterns, these combinators can be used in any context in which a pattern is expected, including nested patterns, the is-pattern-expression, the switch-expression, and the pattern of a switch statement's case label.

pattern
    : disjunctive_pattern
    ;
disjunctive_pattern
    : disjunctive_pattern 'or' conjunctive_pattern
    | conjunctive_pattern
    ;
conjunctive_pattern
    : conjunctive_pattern 'and' negated_pattern
    | negated_pattern
    ;
negated_pattern
    : 'not' negated_pattern
    | primary_pattern
    ;
primary_pattern
    : // all of the patterns forms previously defined
    ;

Open Issues with Proposed Changes

Syntax for relational operators

Are and, or, and not some kind of contextual keyword? If so, is there a breaking change (e.g. compared to their use as a designator in a declaration-pattern).

Should we support some combination of declaration pattern along with a relational pattern? For example,

if (o is int x <= 100) // x is an int with value < 100 here.

Or will the and combinator be sufficient?

if (o is int x and <= 100) // x is an int with value < 100 here.

Semantics (e.g. type) for relational operators

We expect to support all of the primitive types that can be compared in an expression using a relational operator. The meaning in simple cases is clear

bool IsValidPercentage(int x) => x is >= 0 and <= 100;

But when the input is not such a primitive type, what type do we attempt to convert it to?

bool IsValidPercentage(object x) => x is >= 0 and <= 100;

We have proposed that when the input type is already a comparable primitive, that is the type of the comparison. However, when the input is not a comparable primitive, we treat the relational as including an implicit type test to the type of the constant on the right-hand-side of the relational. If the programmer intends to support more than one input type, that must be done explicitly:

bool IsValidPercentage(object x) => x is
    >= 0 and <= 100 or    // integer tests
    >= 0F and <= 100F or  // float tests
    >= 0D and <= 100D;    // double tests

Variable definitions and definite assignment

The addition of or and not patterns creates some interesting new problems around pattern variables and definite assignment. Since variables can normally be declared at most once, it would seem any pattern variable declared on one side of an or pattern would not be definitely assigned when the pattern matches. Similarly, a variable declared inside a not pattern would not be expected to be definitely assigned when the pattern matches. The simplest way to address this is to forbid declaring pattern variables in these contexts. However, this may be too restrictive. There are other approaches to consider.

One scenario that is worth considering is this

if (e is not int i) return;
M(i); // is i definitely assigned here?

This does not work today because, for an is-pattern-expression, the pattern variables are considered definitely assigned only where the is-pattern-expression is true ("definitely assigned when true").

Supporting this would be simpler (from the programmer's perspective) than also adding support for a negated-condition if statement. Even if we add such support, programmers would wonder why the above snippet does not work. On the other hand, the same scenario in a switch makes less sense, as there is no corresponding point in the program where definitely assigned when false would be meaningful. Would we permit this in an is-pattern-expression but not in other contexts where patterns are permitted? That seems irregular.

Related to this is the problem of definite assignment in a disjunctive-pattern.

if (e is 0 or int i)
{
    M(i); // is i definitely assigned here?
}

We would only expect i to be definitely assigned when the input is not zero. But since we don't know whether the input is zero or not inside the block, i is not definitely assigned. However, what if we permit i to be declared in different mutually exclusive patterns?

if ((e1, e2) is (0, int i) or (int i, 0))
{
    M(i);
}

Here, the variable i is definitely assigned inside the block, and takes it value from the other element of the tuple when a zero element is found.

To make this work, we would have to carefully define where such multiple definitions are permitted and under what conditions such a variable is considered definitely assigned.

Should we elect to defer such work until later (which I advise), we could say in C# 9

  • beneath a not or or, pattern variables may not be declared.

Then, we would have time to develop some experience that would provide insight into the possible value of relaxing that later.

Diagnostics, subsumption, and exhaustiveness

These new pattern forms introduce many new opportunities for diagnosable programmer error. We will need to decide what kinds of errors we will diagnose, and how to do so. Here are some examples:

case >= 0 and <= 100D:

This case can never match (because the input cannot be both an int and a double). We already have an error when we detect a case that can never match, but its wording ("The switch case has already been handled by a previous case" and "The pattern has already been handled by a previous arm of the switch expression") may be misleading in new scenarios. We may have to modify the wording to just say that the pattern will never match the input.

case 1 and 2:

Similarly, this would be an error because a value cannot be both 1 and 2.

case 1 or 2 or 3 or 1:

This case is possible to match, but the or 1 at the end adds no meaning to the pattern. I suggest we should aim to produce an error whenever some conjunct or disjunct of a compound pattern does not either define a pattern variable or affect the set of matched values.

case < 2: break;
case 0 or 1 or 2 or 3 or 4 or 5: break;

Here, 0 or 1 or adds nothing to the second case, as those values would have been handled by the first case. This too deserves an error.

byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };

A switch expression such as this should be considered exhaustive (it handles all possible input values).

In C# 8.0, a switch expression with an input of type byte is only considered exhaustive if it contains a final arm whose pattern matches everything (a discard-pattern or var-pattern). Even a switch expression that has an arm for every distinct byte value is not considered exhaustive in C# 8. In order to properly handle exhastiveness of relational patterns, we will have to handle this case too. This will technically be a breaking change, but one no user is likely to notice.

@gafter gafter self-assigned this Oct 2, 2019
@HaloFour

This comment has been minimized.

Copy link
Contributor

commented Oct 2, 2019

GitHub should let me add 🎉and ❤️more than once. I'm especially excited by the prospect of variable patterns being declared in multiple mutually exclusive patterns. F# allows it and it's a powerful feature.

@yaakov-h

This comment has been minimized.

Copy link
Contributor

commented Oct 3, 2019

Specifically, we support all of these relational patterns for sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, and decimal. Additionally, we support == and != for string and bool.

Would we support these patterns for user-defined types that are implicitly convertible to any of those types?

@gafter

This comment has been minimized.

Copy link
Member Author

commented Oct 3, 2019

@HaloFour If you have a use case that really makes sense in C# that would be helpful.

@yaakov-h No, we do not use user-defined conversions in pattern-matching, except to convert the expression for the switch statement once (a necessary nod to compatibility).

@alrz

This comment has been minimized.

Copy link
Contributor

commented Oct 3, 2019

A few points:

  • The or pattern semantics could be applied to multi-label switch sections as well:
    case (var x, 1): case (1, var x):
  • Also, it would be nice to bind identifiers to types (as a fallback when a constant is not available) so we can say x is Int32 or Int64 instead of x is Int32 _ or Int64 _ .
  • Edit: I think having both postfix and prefix relational operators could be very helpful wrt readability.

And there's already some code that could benefit from mutually exclusive pattern variables:

https://github.com/dotnet/roslyn/blob/c1cd335f1306919e68a414fc18f51a3e874ff0dd/src/Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.Analyzer.cs#L310-L317

avoiding code duplication.

@Thaina

This comment has been minimized.

Copy link

commented Oct 3, 2019

This example illustrates our expectation that and will have a higher parsing priority (i.e. will bind more closely) than or

I would like to voice my disagreement on this. and and or and other binary keyword in the same category should take the same precedence and prioritize by left to right in the same way as normal operator. We should always explicitly use parentheses when needed

@Thaina

This comment has been minimized.

Copy link

commented Oct 3, 2019

if ((e1, e2) is (0, int i) or (int i, 0))
{
    M(i);
}

If we would allow this case. Would it be possible for switch to declare same variable in fall through case?

Such as

switch((e1, e2))
{
    case (0, int i):
    case (int i, 0):
        M(i);
        break;
}

or maybe #2703

@Thaina

This comment has been minimized.

Copy link

commented Oct 3, 2019

case >= 0 and <= 100D:

This case can never match

This is unexpected. I expect that we don't need to care about the type of object we put into the comparison operator

byte b = 99;
if(b >= 0 && b <= 100D)
{
    // should be here
}

if(b is >= and <= 100D)
{
    // not come here ??
}
@canton7

This comment has been minimized.

Copy link

commented Oct 3, 2019

public static LifeStage LifeStageAtAge(int age) => age select

Is this a typo, or a sneak peek at a new keyword? 😄

Should we support some combination of declaration pattern along with a relational pattern?

IMO, no. if (o is int x <= 100) was slightly hard to parse for me. if (o is int x and <= 100) is only a few characters longer, and IMO quite a bit clearer.

@ronnygunawan

This comment has been minimized.

Copy link

commented Oct 3, 2019

public static LifeStage LifeStageAtAge(int age) => age select

Is this a typo, or a sneak peek at a new keyword? 😄

Must be a typo. The last code snippet is written using a switch.

@DavidArno

This comment has been minimized.

Copy link

commented Oct 3, 2019

Whilst likely beyond the scope of C# 9, could I please ask that the ideas in the user-defined positional/active patterns threads (#1047 and #277) and support of typeof as a value that can be used in patterns be born in mind when designing the features discussed here? It would be a great shame to make changes now that would make it harder to add active patterns etc later.

@ronnygunawan

This comment has been minimized.

Copy link

commented Oct 3, 2019

if (o is int x and >= 0 and < 10) ...
// or
if (o is int x >= 0 and < 10) ...

I think the intent of this expression is unclear. Will < 10 be evaluated against x or against o?

@canton7

This comment has been minimized.

Copy link

commented Oct 3, 2019

I think the intent of this expression is unclear. Will < 10 be evaluated against x or against o?

True. I skimmed over that without thinking about it too much.

if (o is int x && x >= 0 and < 10)

Clearer, but puts && and and right next to each other, which looks a little jarring.

@DavidArno

This comment has been minimized.

Copy link

commented Oct 3, 2019

Will < 10 be evaluated against x or against o?

Possibly playing devil's advocate here, but does it matter?

@canton7

This comment has been minimized.

Copy link

commented Oct 3, 2019

Possibly playing devil's advocate here, but does it matter?

Given:

case >= 0 and <= 100D:

This case can never match (because the input cannot be both an int and a double).

The answer appears to be yes: if the datatype needs to be the same between the thing being matched and the type of the pattern, then there is a difference between < 10 being applied to an int and an object: it will match for the int but not for the object.

@Thaina

This comment has been minimized.

Copy link

commented Oct 3, 2019

if (o is int x and >= 0 and < 10) ...

I think the intent of this expression is unclear. Will < 10 be evaluated against x or against o?

In my understanding, above is evaluated against o

This

if (o is int x && x is >= 0 and < 10)

is evaluated against x

I think and and or would take the left side of the most nearest left is keyword to be evaluated against it

@YairHalberstadt

This comment has been minimized.

Copy link
Contributor

commented Oct 3, 2019

if the datatype needs to be the same between the thing being matched and the type of the pattern, then there is a difference between < 10 being applied to an int and an object: it will match for the int but not for the object.

Not if we view < 10 as implicitly defining an int check. Then I believe there's no difference and it should be an implementation detail to allow performance optimizations.

@Thaina

This comment has been minimized.

Copy link

commented Oct 3, 2019

@YairHalberstadt How do you think about

float x = 9.9f;
if(x is > 0 and < 10)
{
   // is not matched ?
}
@YairHalberstadt

This comment has been minimized.

Copy link
Contributor

commented Oct 3, 2019

@Thaina

That seems to be the intent of @gafter, that a float would not match > 0.

Whilst conceptually that makes sense. I think it may not be ideal, since it leads to a pit of failure. By default I would probably write something like that, and it won't actually work.

@canton7

This comment has been minimized.

Copy link

commented Oct 3, 2019

I do think that:

float x = 9.9f;
if (x is > 0 and < 10)
{
}

Needs to have exactly the same behaviour (both in terms of any compiler warnings/errors, and runtime behaviour) as:

float x = 9.9f;
if (x > 0 && x < 10)
{
}
@Happypig375 Happypig375 referenced this issue Oct 3, 2019
3 of 5 tasks complete
@CyrusNajmabadi

This comment has been minimized.

Copy link

commented Oct 3, 2019

Note: there is precedence (no pun intended) for the language shipping features that don't behave the same as one would intuit. For example, without using operator-overloading, it's possible to have the following a >= b evaluates to false, but a > b || a == b returns true.

Yes, that's surprising. But it's also NBD in practice. Users rarely (if ever) hit the corners that make this stuff appear. It tends to worry people who think about hte whole language (including myself) and how it all fits together. But it's often overblown as an actual problem for the language and the near total majority of users of it.

@gafter

This comment has been minimized.

Copy link
Member Author

commented Oct 3, 2019

For example, without using operator-overloading, it's possible to have the following a >= b evaluates to false, but a > b || a == b returns true

Please explain.

@gafter

This comment has been minimized.

Copy link
Member Author

commented Oct 3, 2019

Did everyone miss this sentence in the draft spec?

When the input is a type for which a suitable built-in binary relational operator is defined that is applicable with the input as its left operand and the given constant as its right operand, the evaluation of that operator is taken as the meaning of the relational pattern.

@CyrusNajmabadi

This comment has been minimized.

Copy link

commented Oct 3, 2019

Please explain.

Sure!

        int? a = null;
        int? b = null;
        Console.WriteLine(a >= b);
        Console.WriteLine(a > b || a == b);

As defined in the language itself, a is not >= to b. But it is == to b. Leading the above resulting in the false then true being printed above.

This is a core, built-in, inconsistency with the language and how people might generally intuit things to behave.

@LeftofZen

This comment has been minimized.

Copy link

commented Oct 4, 2019

When and why would I prefer x is >= 0 over x >= 0?

@HaloFour

This comment has been minimized.

Copy link
Contributor

commented Oct 4, 2019

@LeftofZen

When and why would I prefer x is >= 0 over x >= 0?

By itself you probably wouldn't prefer to use the pattern, but combined with recursive patterns it can be quite powerful:

if (person is Student { Gpa: >= 3.0 } student) { ... }
@Thaina

This comment has been minimized.

Copy link

commented Oct 4, 2019

Did everyone miss this sentence in the draft spec?

When the input is a type for which a suitable built-in binary relational operator is defined that is applicable with the input as its left operand and the given constant as its right operand, the evaluation of that operator is taken as the meaning of the relational pattern.

@gafter Admittedly this sentence was too hard to understand and picture all the scenario relate to it (at least for me) until we see the example we then know that this sentence was breaking our common sense's expectation

@Thaina

This comment has been minimized.

Copy link

commented Oct 4, 2019

@LeftofZen

When and why would I prefer x is >= 0 over x >= 0?

By itself you probably wouldn't prefer to use the pattern, but combined with recursive patterns it can be quite powerful:

if (person is Student { Gpa: >= 3.0 } student) { ... }

Also when there is

if(some.deep.property.value is >= 0 and <= 100)
@DavidArno

This comment has been minimized.

Copy link

commented Oct 4, 2019

@Thaina,

Of course, there never will be, some.deep.property.value, as that would unlawful according to the Law of Demeter 😉

@Thaina

This comment has been minimized.

Copy link

commented Oct 4, 2019

@DavidArno OK then

if(someVeryLongNamedVariable.WithVeryLongDescriptivePropertyAndLowPerfGet is >= 0 and <= 100)
@alrz

This comment has been minimized.

Copy link
Contributor

commented Oct 4, 2019

There's a proposed alternative syntax that I think has some merits to it:

Syntax for relational operators

Should we support some combination of declaration pattern along with a relational pattern?

if (o is int x <= 100) // x is an int with value < 100 here.

I admit that the original relational pattern syntax take some time to process when combined,

case (>= 0 and <= 10) or (<= 30 and >= 20);

One alternative would be to provide both prefix and postfix forms - it's not just flipping operands around, it has a different meaning altogether.

case (0 <= and <= 10) or (30 >= and >= 20);

However, still nothing stops you to shuffle them around so it doesn't solve the problem completely.

Another approach is to define the syntax in terms of a binary pattern operator,
pattern
    : disjunctive_pattern
    ;
disjunctive_pattern
    : disjunctive_pattern 'or' conjunctive_pattern
    | conjunctive_pattern
    ;
conjunctive_pattern
    : conjunctive_pattern 'and' negated_pattern
    | relational_pattern
    ;    
relational_pattern
    : relational_pattern '<' negated_pattern
    | relational_pattern '>' negated_pattern
    | relational_pattern '<=' negated_pattern
    | relational_pattern '>=' negated_pattern
    | negated_pattern

With one requirement: one operand of a relational pattern must be a constant, exclusively.

This way, you have to specify in what direction the operator applies,

case 0 <= _ <= 10 or 30 >= _ >= 20:
case _ < 10:
case 10 > _:

Of course, this syntax would accept some nonsensical patterns,

case _ < 20 < 1:

But those are already subject to subsumption checking or pattern incompatibility and produces an error - for instance the above example is equivalent to case < 20 and < 1 in the current syntax.

edit: Actually scratch that, it doesn't seem to be aligned with how DAG is constructed.

@ronnygunawan

This comment has been minimized.

Copy link

commented Oct 4, 2019

@alrz
With your proposed syntax, we can also write this:

if (i is 0 < _ <= 10) ...

Powerful.

@alrz

This comment has been minimized.

Copy link
Contributor

commented Oct 4, 2019

That would be a nicer way to say i is > 0 and <= 10, my only concern is nonsensical patterns. I do believe this syntax would do more good than bad, there's an obvious way to write common patterns. with the current syntax there's multiple "right" forms, none of which are preferable to other, also since we have and the problem of nonsensical patterns still exists but no one in their right mind would ever use those.

Even if that turn out to be too much of a problem, I would definitely prefer postfix/prefix, that does help a lot to write meaningful patterns.

@gafter

This comment has been minimized.

Copy link
Member Author

commented Oct 4, 2019

relational_pattern
    : relational_pattern '<' negated_pattern
    | relational_pattern '>' negated_pattern
    | relational_pattern '<=' negated_pattern
    | relational_pattern '>=' negated_pattern

I have no idea what this is intended to convey. This grammar accepts

if (e is < < < >= not var x)

but I have no idea what it is intended to mean.

@alrz

This comment has been minimized.

Copy link
Contributor

commented Oct 4, 2019

if (e is < < < >= not var x)

If I'm not mistaken, no production rule in a primary_pattern is completely optional so I'm not sure how it could accept a sequence of operators?

Anyways, I think the concerete tree that is constructed from that grammar, wouldn't be suitable to build a DAG, so that's probably not a good idea.

I think a simpler approach is to just permit prefix/postfix relational operators, the end result is almost the same: is 0 < and <= 10.

@Unknown6656

This comment has been minimized.

Copy link
Contributor

commented Oct 6, 2019

Whilst likely beyond the scope of C# 9, could I please ask that the ideas in the user-defined positional/active patterns [...] discussed here?

Yeah!!! I would love to see active patterns being available in C#. This would enable us to use regex patterns etc, and I could finally get rid of the extension method I have to write in nearly every project:

using System.Text.RegularExpressions;

public static bool IsMatch(this string input, string regex, out Match match, RegexOptions opt = RegexOptions.Compiled | RegexOptions.IgnoreCase) =>
    (match = Regex.Match(input, regex, opt)).Success;


// ...
if (mystring.IsMatch(@"[a-f0-9]+", out Match m))
{
    // ...
}
@JustNrik

This comment has been minimized.

Copy link

commented Oct 6, 2019

I wish in pattern was included here

bool IsLetter(char c) => c is in 'a'..'z' or 'A'..'Z';

in could be be translated into an array with the range and use Contains

@Eirenarch

This comment has been minimized.

Copy link

commented Oct 6, 2019

I am in favor of adding as much exhaustiveness warnings/errors as reasonably possible. I somehow feel this adds a lot of values to pattern matching compared to a bunch of ifs

@Richiban

This comment has been minimized.

Copy link

commented Oct 7, 2019

I wish in pattern was included here

bool IsLetter(char c) => c is in 'a'..'z' or 'A'..'Z';

in could be be translated into an array with the range and use Contains

Except, for some reason I still do not understand, ranges are end-exclusive. So, to test for inclusion, you have to add 1 to the right-hand side:

bool IsLetter(char c) => c is in 'a'..'{' or 'A'..'[';
@HaloFour

This comment has been minimized.

Copy link
Contributor

commented Oct 7, 2019

@Richiban

Except, for some reason I still do not understand, ranges are end-exclusive.

I recall that range patterns would be inclusive. I also believe that to was suggested as an operator instead of .. to differentiate the behavior.

#812

But it sounds like this issue subsumes that issue and that combining relational patterns with combinator patterns would be used in place of range patterns.

@setbbtjl8wwq

This comment has been minimized.

Copy link

commented Oct 14, 2019

@gafter

if (e is not int i) return;
M(i); // is i definitely assigned here?

IMHO, there is a more clear alternative when not is right side synonym of negation operator !

/* negation */
public static bool Not(this bool b) => !b
if (!(e is int i)) 
if (e is int i not)

if(!e.Is(out int i))
if(e.Is(out int i).Not())

/* relaxed 'out'  */
if(!e.Is(int i))
if(e.Is(int i).Not())

See relaxed out #2818

Seems that mathematical notation looks more pretty for intervals

bool IsValidValue(int x) => x is > 0 and < 8;
bool IsValidValue(int x) => x is >= 0 and < 8;
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or (=='~');

bool IsValidValue(int x) => x is in ]0, 8[;
bool IsValidValue(int x) => x is in [0, 8[;
bool IsLetter(char c) => c is { in ['a', 'z'] or in ['A', 'Z'] or '~' };

bool IsValidValue(int x) => x is in (0..8);
bool IsValidValue(int x) => x is in [0..8);
bool IsLetter(char c) => c is { in ['a'..'z'] or in ['A'..'Z'] or '~' };

For open intervals it is possible to use something like

bool IsValidValue(int x) => x is in (..8); /* x < 8 */
bool IsValidValue(int x) => x is in (0..); /* x > 0 */

The most perturbing point is constant oriented syntax of pattern-matching (hardcode style).
For example we have

bool IsValid(int x) => x is (>= 0 and <= 10) or (>= 20 and <= 100) or (==128);

Then we want to change one hardcoded value to dynamic

bool IsValid(int x) => x is (>= 0 and <= 10) or (>= 20 and <= Settings.MaxValidValue) or (==128);

Now we will got a compile error and should significantly rearrange the code style... Why?

@orthoxerox

This comment has been minimized.

Copy link

commented Oct 14, 2019

if (e is int i not)

Shouldn't this be if (e is int i... not!)?

@hez2010

This comment has been minimized.

Copy link

commented Oct 14, 2019

How about multiple values matching?

var result = number switch
{
    1 | 2 | 3 | 4 => "1~4",
    5 | 6 | 7 | 8 => "5~8",
    > 8 => ">8"
    _ => "<1"
};
@svick

This comment has been minimized.

Copy link
Contributor

commented Oct 14, 2019

@hez2010

You will be able to do that using the or pattern:

var result = number switch
{
    1 or 2 or 3 or 4 => "1~4",
    5 or 6 or 7 or 8 => "5~8",
    > 8 => ">8"
    _ => "<1"
};
@reflectronic

This comment has been minimized.

Copy link

commented Oct 15, 2019

conjunctive_pattern
    : conjunctive_pattern 'or' negated_pattern
    | negated_pattern
    ;

Did you mean to do conjunctive_pattern 'and' negated_pattern?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.