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

Proposal: Simplified Property Patterns #8457

Closed
HaloFour opened this issue Feb 8, 2016 · 29 comments
Closed

Proposal: Simplified Property Patterns #8457

HaloFour opened this issue Feb 8, 2016 · 29 comments

Comments

@HaloFour
Copy link

HaloFour commented Feb 8, 2016

I had mentioned this in #206 but I wanted to open a separate proposal to consider it. In my opinion the current syntax for property patterns can be a little weird looking given that the properties are matched to subpatterns. I think that it would be easier to read to have simpler syntax for the simple cases.

Constant Pattern

The constant pattern can be compared using the equality operator instead of is. There is no real improvement to verbosity here but it appears to look more like a normal comparison. The same rules apply in that the operand must be a constant.

if (obj is Person { LastName == "Gates" }) { ... }

// equivalent to
if (obj is Person { LastName is "Gates" }) { ... }

Variable Pattern

The variable pattern looks more like an inline variable declaration and should feel immediately familiar.

if (obj is Person { var lastName = LastName }) {
    Console.WriteLine($"Hello Mr. {lastName}");
}

// equivalent to
if (obj is Person { LastName is var lastName }) {
    Console.WriteLine($"Hello Mr. {lastName}");
}

Inline Guards

Inline guards allow to put simple comparison logic directly into the property pattern without having to use the variable pattern combined with when guard clauses.

string lastName = "Gates";
if (obj is Person { LastName == lastName, Age >= 60 }) { ... }

// equivalent to
string lastName = "Gates";
if (obj is Person { LastName is var $tmp0, Age is var $tmp1 }
    && $tmp0 == lastName && $tmp1 >= 60) { ... } 

Possibly also support inline bool expressions:

string pattern = "(?i)(Gates|Ballmer|Nadella)";
if (obj is Person { Regex.Matches(LastName, pattern) }) { ... }

// equivalent to
if (obj is Person { LastName is var $tmp0 } && Regex.Matches($tmp0, pattern)) { ... }

Known Type

Can omit the type name and the compiler will perform the property pattern using the known type.

Person person = ...;
if (person is { LastName == "Gates", var firstName = FirstName }) { ... }

// equivalent to
if (obj is Person { LastName is "Gates", FirstName is var firstName }) { ... }

Update: Fixed guard constructs since when doesn't apply to if statements.

@alrz
Copy link
Contributor

alrz commented Feb 8, 2016

Finally 👍

But I think inline bool expression doesn't add much and complicates the pattern production rules (that is why we have case guards).

One suggestion, I'd prefer simple var for variable patterns,

if (person is { LastName == "Gates", FirstName var firstName }) { ... }

That doesn't totally change the order of leading identifier in the property subpatterns.

@HaloFour
Copy link
Author

HaloFour commented Feb 8, 2016

@alrz

That doesn't totally change the order of leading identifier in the property subpatterns.

In my opinion it's the order that I think feels the most confusing with variable patterns. Everywhere else in the language assignment is right-to-left. I think mimicking normal variable assignment operator syntax makes it immediately recognizable as an assignment. I know that people who are more familiar with F# should be familiar with record patterns assigning left-to-right, but they would probably also not have as many issues groking is var foo or some variation.

I've heard similar complaints about the type pattern, in that it looks and feels weird for the identifier to be assigned to appear on the right.

@alrz
Copy link
Contributor

alrz commented Feb 9, 2016

@HaloFour Downside of using variable declaration syntax for var pattern is that if you want to explicitly state the type (type check) you'd end up with something like this,

if(p is { Bar bar = Foo }) { ... }

but the semantics is more like let here,

if(p is { let Bar bar = Foo }) 

Which is more verbose and (I don't know the word) than is var, while with my suggestion you could still use the regular is for type checks, as you always would do

if(p is { Foo var foo }) 
if(p is { Foo is Bar bar }) 

And as you suggested, if we use == for comparing constants, there will be no ambiguity to omit the variable in type checks,

if(p is { Foo is Bar })

@HaloFour
Copy link
Author

HaloFour commented Feb 9, 2016

@alrz That is a potential issue, although I don't think that it's a particularly big deal. The alternative, which is what I had mentioned in my comment on #206 originally, was that when using that variable declaration pattern specifying the type name explicitly would not behave as if you were applying the type pattern. Instead it would be treated as a compile-time checked assignment:

public class Person {
    public string Name { get; set; }
}

public class Student : Person {
    public Course Course { get; set; }
}

Person person = new Student() { ... };

// legal
if (person is Student { string name = Name, Course course = Course }) { ... }
// compiler error, CS0266 Cannot implicitly convert type 'Course' to 'OnlineCourse'
if (person is Student { string name = Name, OnlineCourse course = Course }) { ... }
// legal
if (person is Student { string name = Name, Course is OnlineCourse course }) { ... }

I personally prefer this syntax and behavior despite its lack of symmetry with the variable and type patterns.

@alrz
Copy link
Contributor

alrz commented Feb 9, 2016

Honestly, it doesn't look like a pattern anymore. But the rest is all good, for example omitting known types would also work with anonymous types,

var p = new { Foo = "foo" };
switch(p) {
  case { Foo == "foo" }: ...
}

@alrz
Copy link
Contributor

alrz commented Feb 9, 2016

Although this is contrary to the title but I'd like to be able to use OR patterns also in property patterns,

// distributive operators
if(a is T { P == 5 or 10 })

// disjunctive patterns
if(a is T(5 or 10))

Related: #6235

@Makoto64
Copy link

Makoto64 commented Feb 9, 2016

The OR pattern is a luxury I would be thrilled to have at my disposal.

@HaloFour
Copy link
Author

Having thought about these proposed syntax additions I've come up with a few additional cases where this syntax would differ from the pattern form. I don't think that this is necessarily a problem but it does need to be considered.

  1. Variable Pattern and Implicit Conversions

    I had proposed the syntax var x = Y as an alternate assignment-like form of the variable pattern. In the comments I also mentioned having support for a non-inferred assignment, e.g. string x = Y, which would differ in behavior from its the type pattern in that it would be a compile-time error if the type of the property cannot be implicitly converted to the specified type. Simple enough, but this glosses over implicit conversion operators.

    I do think that there is a bit of a can-of-worms in allowing for the implicit type conversion to be considered. If implicit type conversions, why not also explicit type conversions, e.g.:

    if (p is P { Z z = (Z)Y })

    And if explicit type conversions, why not arbitrary expressions, e.g.:

    if (p is P { Z z = MyFunc(Y) })

    There might be some utility to supporting these kinds of expressions within a pattern, and the compiler could certainly translate it, but the question is whether its worth complicating the pattern property syntax to make it possible.

    if (p is P { Z z = MyFunc(Y) }) { Console.WriteLine(z); }
    // translates to
    if (p is P { Y is var $tmp }) {
        Z z = MyFunc($tmp);
        Console.WriteLine(z);
    }
  2. Constant/Comparison Patterns

    I had also proposed the equality comparison expression P == "Foo" as an alternate form of the constant pattern P is "Foo". However, similar to the issue above there is the consideration of custom equality operators as well as implicit conversion operators that may come into play if such a comparison were made as a normal expression. If "inline guards" are also considered this is probably not a big problem as the expression could be lifted into such a guard:

    if (p is P { Z == "foo" }) { ... }
    // translates to
    if (p is P { Z is var $tmp } when ($tmp == "foo")) { ... }

@alrz
Copy link
Contributor

alrz commented Feb 17, 2016

@HaloFour I think it's all taken care of in the pattern spec draft,

A value of static type E is said to be pattern compatible with the type T if there exists an identity conversion, an implicit reference conversion, a boxing conversion, an explicit reference conversion, or an unboxing conversion from E to T.

Using assignment-like deconstruction and separating each of these conversations in each case for a pattern would complicate those rules and wouldn't be intuitive at all.

@HaloFour
Copy link
Author

@alrz That doesn't cover user-defined conversion operators, only the language supported implicit/explicit reference conversions. I seriously doubt that the type pattern would consider conversion operators because that would result in a disparity with the is operator.

@alrz
Copy link
Contributor

alrz commented Feb 24, 2016

I would like to suggest another level of conciseness for property patterns when the type is known.

Under this proposal,

expr is MemberExpression { Member is MemberInfo { Name is var name } }

becomes

expr is MemberExpression { Member is { Name is var name } }

It would be nice to be able to dot off it if you want to match a nested property,

expr is MemberExpression { Member.Name is var name }

Obviously, if Member was null pattern fails.

@paulomorgado
Copy link

@HaloFour, are you proposing to have both forns or for your proposal to be the one proposal?

@HaloFour
Copy link
Author

@paulomorgado

I think I'd keep the arbitrary subpatterns around since that syntax would be used elsewhere in pattern matching. And if any of my proposed syntax must behave differently due to the expectations of the syntax then I think it would be important to be able to utilize the stock pattern matching behaviors.

@paulomorgado
Copy link

if (obj is Person { var lastName = LastName }) {
    Console.WriteLine($"Hello Mr. {lastName}");
}

What would happen if a LastName was already in scope?

@HaloFour
Copy link
Author

@paulomorgado Within the property pattern that identifier would refer specifically to the property. Any other identifier by that name in scope wouldn't matter.

@alrz
Copy link
Contributor

alrz commented Mar 3, 2016

@bondsbw Commenting in the related thread.

So what would be the impact if is does not work with constant patterns, in general?

As currently specified is outside of property patterns doesn't accept constant patterns, per this proposal I expect something like this could be allowed in property subpatterns.

property-subpattern:
identifier < shift-expression
identifier > shift-expression
identifier <= shift-expression
identifier >= shift-expression
identifier == shift-expression
identifier != shift-expression
identifier is complex-pattern
identifier is type
identifier var identifier

Compared to:

relational-expression:
shift-expression
relational-expression < shift-expression
relational-expression > shift-expression
relational-expression <= shift-expression
relational-expression >= shift-expression
relational-expression is type
relational-expression is complex-pattern

Difference between existing is operator and is operator in patterns is just that. the latter accepts constants (that can be an identifier) and the former accepts a type that is an identifier. But using relational operators there woudn't be any ambiguity anymore.

@bondsbw
Copy link

bondsbw commented Mar 3, 2016

@alrz Those could definitely help, but I'm not sure if I would be comfortable with having those as subpatterns without allowing the full breadth of expression syntax, such as what @HaloFour mentioned:

obj is Person { Regex.Matches(LastName, pattern) }

It might be more frustrating for users considering some types of expressions work and some don't.

Also could there be alternative ways to accomplish the same goals? Such as

e is Employee { Id is GreaterThanOrEqualTo(0) }

index is Between(0, str.Length)

obj is Person { LastName is RegexMatch(pattern) }

Forgive me if this is already covered in pattern matching, I get lost on some of those larger threads where the ideas have evolved.

@bondsbw
Copy link

bondsbw commented Mar 3, 2016

Or even implicit conversion of infix binary operators to prefix unary:

e is Employee { Id is >= 0 }

@alrz
Copy link
Contributor

alrz commented Mar 3, 2016

obj is Person { Regex.Matches(LastName, pattern) }

This is extensively ambiguous. How the compiler should know that which identifiers are Person's properties? That said, I think the leading identifier in property subpatterns is a must.

Also, using #9005

obj is Person { LastName is RegexMatch(pattern) }

It doesn't make sense to turn "a pattern that matches against an identifier" to a full blown expression.

@bondsbw
Copy link

bondsbw commented Mar 3, 2016

@alrz Sorry, I wasn't clear since I didn't copy all of @HaloFour's example. pattern was used as a variable (not a placeholder for a syntax pattern):

string pattern = "(?i)(Gates|Ballmer|Nadella)";

@alrz
Copy link
Contributor

alrz commented Mar 3, 2016

@bondsbw I'm trying to show you the alternative way (using active patterns) to do that which I think is not ambiguous, so it's not totally impossible.

pattern was used as a variable (not a placeholder for a syntax pattern):

Yes, that's what you know but the compiler doesn't.

@bondsbw
Copy link

bondsbw commented Mar 3, 2016

@alrz

Yes, that's what you know but the compiler doesn't.

Nonsense, it is completely unambiguous that pattern was a string variable. How do you come to any other conclusion?

@alrz
Copy link
Contributor

alrz commented Mar 3, 2016

@bondsbw How about LastName? For each identifier the compiler should check target type (in this case Person) to see if it has a property with that name and match agaist it, otherwise it should lookup the variable in the containing scope? And what happens if you actually add such variable/property in that scope later? There will be some crazy scoping rules that is not really necessary IMO. All in all, I don't have a strong disagreement towards any of this because I'm not quite OK with the current form either. So I'd rather wait to see what it will become :shrugs:

@bondsbw
Copy link

bondsbw commented Mar 3, 2016

@alrz Ok, I was under the impression that you had issue with what I mentioned. Your issue seems to be at a much more fundamental level, with the general form

property-subpattern:
    identifier is complex-pattern

and the ambiguity surrounding the resolution of identifier.

But I don't think that's necessarily a difficult problem, right? At the top level of such a pattern, the identifier has to be in the local scope. Inside a property subpattern, it would have to be a member name on the object in question. Examples:

var person = new Person { LastName = "Gates" };
var LastName = "Smith";
if (person is { LastName is "Gates" }) // ok
{                                      // person is resolved from containing scope and
    ...                                // LastName is resolved as a member on person:Person
}

var person = new Person { LastName = "Gates" };
var length = person.LastName.Length;
if (person is { length is 5 }) // error
{                              // length is not a member of person:Person
    ...                        // length from containing scope cannot be used as property subpattern identifier
}

var person = new Person { LastName = "Gates" };
var length = person.LastName.Length;
if (person is Person && length is 5) // ok
{                                    // length is resolved from containing scope
    ...
}

var person = new Person { LastName = "Gates" };
if (person is { LastName is { Length is 5 }}) // ok
{                                             // LastName is resolved as a member of person:Person
    ...                                       // Length is resolved as a member of LastName:string
}

@gafter
Copy link
Member

gafter commented Mar 10, 2016

The constant pattern can be compared using the equality operator instead of is.

Except that they have different semantics.

    object o = (short)3;
    System.Console.WriteLine(o == 3); // error: operator== cannot be applied to operands of type `object` and `int`
    System.Console.WriteLine(o.Equals(3)); // False

@HaloFour
Copy link
Author

@gafter

Except that they have different semantics.

On top of the other issues that I pointed out above. I do agree that the one could not be considered an alternative to the other. At best, shorthand for a pattern guard:

if (person is Student { GPA == 4.0 }) { ... }
// equivalent to
if (person is Student { GPA is var $tmp } && $tmp == 4.0) { ... }

or

case Student { GPA == 4.0 }:
// equivalent to
case Student { GPA is var $tmp } when $tmp == 4.0:

While I do think that the syntax would be nice and concise and relatively easy to understand I'm worried that it does open a can of worms.

This proposal is more about kicking around ideas for bridging the gap between the syntax of C# 6.0 and earlier and the proposed syntax for pattern matching, particularly for those that don't have a lot of experience with pattern matching in functional languages, which I assume to be the majority of C# developers. I think syntax similar to this would ease people into the concepts behind pattern matching and make the feature easier to grasp. But there is also something to be said about just pulling that bandaid, particularly if the syntax offers no other benefits.

@gafter
Copy link
Member

gafter commented Mar 10, 2016

@HaloFour Matching against the pattern 4 is not the same as saving to a variable and then comparing against 4, in part because that variable might be of type object and therefore not of the correct type. So no, it is not the same as a pattern guard.

@HaloFour
Copy link
Author

@gafter I agree, that's what I was trying to say. If a syntax like Student { GPA == 4.0 } is considered I would treat that like a guard condition and not like the constant pattern. In that case, if GPA happened to be of type object then that comparison would be a compile-time error as operator == cannot be applied to operands of type object and double.

@gafter
Copy link
Member

gafter commented Apr 21, 2017

Issue moved to dotnet/csharplang #480 via ZenHub

@gafter gafter closed this as completed Apr 21, 2017
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

7 participants