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

Features of a pattern matching syntax #337

Open
zspitz opened this issue Aug 28, 2018 · 25 comments

Comments

@zspitz
Copy link

@zspitz zspitz commented Aug 28, 2018

What features / attributes should a VB.NET pattern matching syntax have?

I propose the following:

  • Allow the same syntax for patterns in a boolean-returning expression (introduced with a dedicated keyword), as for patterns in a Case clause. This enables patterns to be used as the condition of If ... Then, Do While ... blocks and the like.
  • Allows the pattern to introduce variables into the child scope -- this will be either the Case block or the block following the match expression
  • Pattern-neutral, not oriented specifically to a type-checking pattern
  • Allow When clauses for additional conditions
  • Allows for nested patterns -- certain pattern types can contain other patterns

I've written some examples, and a proposed spec in line with these goals here. But the purpose of this issue is to discuss whether these goals are valid and relevant.

@zspitz

This comment has been minimized.

Copy link
Author

@zspitz zspitz commented Dec 15, 2018

One other point:

@ericmutta

This comment has been minimized.

Copy link

@ericmutta ericmutta commented Dec 15, 2018

@zspitz I've written some examples, and a proposed spec in line with these goals here.

Thanks for thinking about this feature in detail and going as far as writing a grammar!

I think the goals are reasonable though I would add one more: keep it really really really simple.

The simplicity goal matters a lot because every feature is a product: it needs a spec, it has to be implemented in the compiler, implemented in the IDE, tested in all scenarios, documented (with samples) and then supported forever. This is a lot of work!

So to keep it simple, we should find the minimum viable functionality for this feature, hope that gets implemented and then expand from there in the future (otherwise none of it will ever get done in our lifetime).

From my own experience with the feature in C#, the type checking pattern is really all we need (and the C# docs seem to focus on mostly that). And we could probably do it without adding a new keyword and sticking to Is, so borrowing from some of the examples you gave:

'test the type without introducing a variable (not strictly necessary but...)
If obj Is String Then Console.WriteLine("obj is a string")

'test the type and introduce a variable.
If obj Is s As String Then Console.WriteLine(s.Length)

'can also assign the boolean result to a local variable
'(useful for extending scope of variable "s" beyond block that checks test1/test2).
Dim test1 = obj Is String
Dim test2 = obj is s As String

'we should probably use AndAlso instead of When for simplicity.
If obj Is s As String AndAlso s.Length > 5 Then Console.WriteLine(s.Length)

'can do the above with Select too:
Select Case obj
  Case Is String
    Console.WriteLine("obj is a string")
  Case Is s As String
    Console.WriteLine(s.Length)
  Case Is s As String AndAlso s.Length > 5
    Console.WriteLine(s.Length)
End Select

'...and in loops that expect a boolean condition:
Do While obj Is s As String
 Process(s)
 obj = GetNextObject()
Loop

Do Until obj Is s As String
  ProcessObjectsThatAreNotStrings(obj)
Loop

In short:

  1. We are introducing a boolean-valued type checking and (optional) conversion expression.
  2. It returns false if the type check fails and sets any introduced variable to Nothing.
  3. It returns true if the conversion succeeds and assigns the converted value to the introduced variable.
  4. The introduced variable (if any) is scoped to the block containing the expression.

If for example the above functionality is all we got in order to keep things as simple as possible, does anyone feel something critical is missing? Remember: we can get really fancy with this, but if we do, it is unlikely to ever get implemented because it will be too much work!

CC: @KathleenDollard some food for thought here for the LDM as you consider this feature.

@paul1956

This comment has been minimized.

Copy link

@paul1956 paul1956 commented Dec 16, 2018

Generally I love it and would use this functionality constantly it would dramatically simplify code.
Questions
Can "If obj Is s As String Then Console.WriteLine(s.Length)" have an else case? If not, then how would "sets any introduced variable to Nothing", ever be tested or be useful? The variable would not be in scope. If there is an else what Type is "s" certainty not String.

In the Do Until, once you fall out of the loop "s" is not in scope so the assignment seems useless, so I am not sure if the assignment feature is useful or could be tested.

@ericmutta

This comment has been minimized.

Copy link

@ericmutta ericmutta commented Dec 16, 2018

@paul1956 Can "If obj Is s As String Then Console.WriteLine(s.Length)" have an else case? If not, then how would "sets any introduced variable to Nothing", ever be tested or be useful? The variable would not be in scope. If there is an else what Type is "s" certainty not String.

I imagine it should work like this:

If obj Is s As String Then 
  'can use variable "s" here and it will be a non-null string.
Else
  'can't use variable "s" here because it is not in scope.
End If

@paul1956 In the Do Until, once you fall out of the loop "s" is not in scope so the assignment seems useless,

That's a good point! It would be allowed for completeness much like assigning a variable to itself is allowed and equally useless. Though there could be some utility in the version that doesn't introduce a variable:

Do Until obj Is String
  ProcessNonStringObject(obj)
  obj = GetNextObject()
Loop
@zspitz

This comment has been minimized.

Copy link
Author

@zspitz zspitz commented Dec 16, 2018

(An updated version is here.)

@ericmutta In thinking about what the minimum viable functionality would look like, I've reached the following conclusions (I apologize for the length).

TL;DR

  1. Extend Is and Case for pattern matching -- <expression> Is <pattern> and Case <pattern>; instead of a dedicated keyword that introduces a pattern.

  2. The same syntax should work with both <expression> Is <pattern> and Case <pattern>

    2a. Is should be extended to allow all current Case syntax -- Case <expression> To <expression>, Case <expression>, <expression>; and should use value equality where available

    2b. Case should be extended for all current Is syntax -- Case <expression> should support reference equality and comparison against Nothing (#119)

  3. Since Case and Is already support simple expressions, anything until the end of the condition / Case clause must be considered part of the pattern, to prevent ambiguity.

    3a. The only way to allow for additional logic AFAICT is to wrap it in a single-per-pattern-expression When clause.

    3b. Trying to reuse AndAlso for this purpose will only increase complexity.

  4. Case should continue to not require Is.

(Possible grammar at the end.)


If pattern matching is just another type-checking syntax (admittedly also allowing introduced variables), then I don't think the feature can possibly justify itself. (In fact, the compiler could do the same thing without any additional syntax.)

But pattern matching is so much more than type-checking -- it's saying:

  • does the expression on the left match the pattern expressed by some syntax on the right?
  • if it does, assign parts of whatever is being matched to new identifiers

From my own experience with the feature in C#, the type checking pattern is really all we need (and the C# docs seem to focus on mostly that).

Pattern matching in C# is still in its infancy; see the F# documentation for a better idea of the range of potential patterns.

Having said that, you are quite correct -- it's going to be harder to add all the possible patterns right at the beginning. However, any first steps in pattern matching must specify at least two things:

  • What syntax introduces a pattern? Also, where does the pattern end?
  • One "killer app" pattern, e.g. the variable-as-type pattern

It also should allow for the goals outlined above, even if these goals aren't implemented immediately; and should also be compatible and consistent with existing syntax.


AFAICT there are two possible ways to insert pattern matching into the language:

  • Introduce a new dedicated keyword (e.g. Matches)
  • Extend the behavior of the existing Case and Is

I prefer extending Case/Is, because:

  • Case already has a (albeit very limited) form of pattern matching
  • Having a new keyword means users of the language have to remeber yet another syntax
  • There is already an inverse for Is in the conditions of If statements and the like: IsNot. For a dedicated keyword, it would be necessary to choose between no inverse -- If Not o Matches String Then -- or something equally awkward..

although a dedicated keyword might be simpler to implement, because we wouldn't have to deal with the historical usages of Is and Case.


If we extend Case/Is, we shouldn't require Is for patterns in Case clauses. Forcing Is to be required, would result in three different rules for whether Case needs to be followed by Is:

  • Case <expression> -- the Is cannot be used, per the spec
  • Case Is <operator> <operand> -- the Is is optional, also per the spec
  • Case Is <pattern> -- the Is is required

which I think would be very confusing.


Also, assuming we extend Case/Is, since VB.NET already supports both Case <expression> and Is <expression>, any other expressions until the end of the condition / Case clause must be considered part of the pattern. Otherwise the following would be ambiguous:

Dim o As Object
Dim foo As Boolean
Dim bar As Boolean
If o Is foo AndAlso bar Then

Should foo AndAlso bar be evaluated first? Or should o Is foo be evaluated first?

C# doesn't have this problem, because C# doesn't allow case with an expression, so the following:

object o = null;
bool bar = true;
if (o is bool b && bar)

is unambiguous -- first perform the pattern match, then the logical AND.

(NB. OTOH, this does simplify things a little, because we don't need an explicit literal pattern.)


If a pattern expressions must be greedy (i.e. go to the end of the condition / Case clause), then extra logic at the end of the pattern cannot use a simple boolean expression; we need a special clause for this purpose. C# introduces this clause with the when keyword.

Specifically in VB.NET, we can't simply reuse an existing keyword such as AndAlso to introduce this clause, because AndAlso might be part of the expression in the pattern.

We could make up all sorts of complicated rules to disambiguate, but the simplest would be to introduce a new keyword -- When.

(NB. A similar usage of When already exists in VB.NET, when applying a condition to a Catch exception.)


Taking into account the need to define the following from the start:

  • where a pattern expresion (an expression containing a pattern match) can be used,
  • what are the parts of a pattern expression,
  • patterns that cover the existing behaviors of both Case and Is
  • the additional new variable+typecheck pattern

I think the following is the "minimum viable functionality":

Where a pattern expression can be used:

BooleanOrPatternExpression
    : BooleanExpression
    | Expression 'Is' PatternExpression
    ;


// If...Then..ElseIf blocks

BlockIfStatement
    : 'If' BooleanOrPatternExpression 'Then'? StatementTerminator
      Block?
      ElseIfStatement*
      ElseStatement?
      'End' 'If' StatementTerminator
    ;

ElseIfStatement
    : ElseIf BooleanOrPatternExpression 'Then'? StatementTerminator
      Block?
    ;

LineIfThenStatement
    : 'If' BooleanOrPatternExpression 'Then' Statements ( 'Else' Statements )? StatementTerminator
    ;


// Loops

WhileStatement
    : 'While' BooleanOrPatternExpression StatementTerminator
      Block?
      'End' 'While' StatementTerminator
    ;

DoTopLoopStatement
    : 'Do' ( WhileOrUntil BooleanOrPatternExpression )? StatementTerminator
      Block?
      'Loop' StatementTerminator
    ;

// cannot introduce variables in to child scope here
DoBottomLoopStatement
    : 'Do' StatementTerminator
      Block?
      'Loop' WhileOrUntil BooleanOrPatternExpression StatementTerminator
    ;

ConditionalExpression
    : 'If' OpenParenthesis BooleanOrPatternExpression Comma Expression Comma Expression CloseParenthesis
    | 'If' OpenParenthesis Expression Comma Expression CloseParenthesis
    ;


// Within a Case clause

CaseStatement
    : 'Case' PatternExpression StatementTerminator
      Block?
    ;

What are the parts of a pattern expression?

PatternExpression
    : Pattern ('When' BooleanExpression)?
    ;

What patterns should be supported from the start?

Pattern
    // patterns with subpatterns
    : Pattern ',' Pattern                    // OR pattern (already supported in Case)

    // patterns without subpatterns
    | 'Of' TypeName                          // Type check pattern -- matches when subject is of TypeName
    | Identifier 'As' TypeName               // Variable pattern -- introduces a new variable in child scope
    | 'Is'? ComparisonOperator Expression    // Comparison pattern
    | 'Like' StringExpression                // Like pattern
    | Expression 'To' Expression             // Range pattern
    | Expression                             // Equality pattern -- value/reference equality test against Expression
    ;
@zspitz

This comment has been minimized.

Copy link
Author

@zspitz zspitz commented Dec 16, 2018

@paul1956

This comment has been minimized.

Copy link

@paul1956 paul1956 commented Dec 17, 2018

If the powers in charge can agree on a grammar and ultimate feature set, it would be nice to get "Is TypeName" and "Is Identifier As TypeName" out first. If all the things being proposed this is the one thing needed yesterday and would simplify lots of VB Code..

@ericmutta

This comment has been minimized.

Copy link

@ericmutta ericmutta commented Dec 17, 2018

@zspitz Otherwise the following would be ambiguous:

Dim o As Object
Dim foo As Boolean
Dim bar As Boolean
If o Is foo AndAlso bar Then

It isn't ambiguous because it doesn't compile at all! Compiler says Is operator does not accept operands of type Boolean. Operands must be reference or nullable types.

Ultimately the choice between AndAlso and When is likely to be a matter of preference, mainly because both keywords already exist in the language so nothing new is being added, regardless of which you choose. I personally prefer 'AndAlso' mainly because it makes clear the short-circuiting nature of the expression (i.e the part to the right of AndAlso will not run if the type-check and conversion on the left did not succeed).

@zspitz I've reached the following conclusions (I apologize for the length).

I believe that when it comes down to implementing this, the team will appreciate the level of detail in your comments, it is clear you have given this a lot of thought! 👍 👍

@zspitz

This comment has been minimized.

Copy link
Author

@zspitz zspitz commented Dec 17, 2018

@ericmutta

Thanks for the kind words; I hope you're right, and this will enable the team to move forward on pattern matching that much faster.

It isn't ambiguous because it doesn't compile at all!

If o Is foo currently doesn't compile, as you've noted. But if we extend Is to follow everything Case does today, then If o Is foo Then would compile just fine.

But even then, trying to push more boolean expressions onto the pattern would be ambiguous.

@bandleader

This comment has been minimized.

Copy link

@bandleader bandleader commented Dec 17, 2018

o Is foo AndAlso bar -- It isn't ambiguous because it doesn't compile at all! Compiler says Is operator does not accept operands of type Boolean. Operands must be reference or nullable types.

@ericmutta In addition to what @zspitz noted above -- it's nevertheless ambiguous in parsing. The fact that bar is Boolean is not known until the binding stage of the compiler. (Also, compilers should anyway never parse differently based on semantics.)

This distinction (between lexical and semantic analysis -- done by the parser and binder respectively) is often overlooked in these discussions. More about that later.

@bandleader

This comment has been minimized.

Copy link

@bandleader bandleader commented Dec 17, 2018

  1. Another issue with extending Is to work with patterns and supporting an expression as a pattern: Is currently checks for reference equality, whereas expressions would presumably check for equality including IEquatable equality/.Equals (and it would have to because that's what Case does as well). So obj Is otherObjWhichEquatesToObj would suddenly have to return True. Aside from breaking existing code, there's also the fact that VB does need a reference equality operator.
@bandleader

This comment has been minimized.

Copy link

@bandleader bandleader commented Dec 17, 2018

  1. Problem: Case x currently does a value check. Case x As String is being proposed to do a typecheck-and-assign. This is very non-intuitive.

(For comparison, the following two lines do the same thing both lexically and in human understanding, just with specific types vs. type inference. This is critical for intuition and should be a hard requirement for any VB syntax which can take an As T or skip it.)

For Each x In collection
For Each x As String In collection

Same for:

Dim x = expr
Dim x As String = expr

I have some more thoughts against re-using Select Case and Is as-is for pattern matching, or at least for this pattern (typecheck-and-assign), even while realizing the similarity between them and the desire to mesh it all together. I'll try to post later.

@bandleader

This comment has been minimized.

Copy link

@bandleader bandleader commented Dec 18, 2018

@zspitz Most importantly -- thank you for your amazing efforts in moving this forward. Here's hoping MS will take notice and reciprocate!

@zspitz

This comment has been minimized.

Copy link
Author

@zspitz zspitz commented Dec 18, 2018

Another issue with extending Is to work with patterns and supporting an expression as a pattern: Is currently checks for reference equality, whereas expressions would presumably check for equality including IEquatable equality/.Equals (and it would have to because that's what Case does as well). So obj Is otherObjWhichEquatesToObj would suddenly have to return True. Aside from breaking existing code, there's also the fact that VB does need a reference equality operator.

Agreed. I think we'd need a dedicated keyword for boolean contexts If <expression> Matches <pattern> Then, Do While <expression> Matches <pattern> etc. (I'll update my original post accordingly.)

But I still think we should extend Select Case to patterns in general, without an additional keyword. Otherwise users will have to decide whether to use Case Matches <pattern> or Case <expression>, each with its own limitations.

@KathleenDollard

This comment has been minimized.

Copy link
Contributor

@KathleenDollard KathleenDollard commented Dec 18, 2018

In general, I like letting the discussion flow without interrupting it.

This has been a great discussion. I appreciate everyone's work, and wanted to be sure you all didn't think you were talking into a void. It's also the start of Christmas holidays, which means a lot of folks are out until the first of the year.

@bandleader

This comment has been minimized.

Copy link

@bandleader bandleader commented Dec 18, 2018

@KathleenDollard Appreciated, but I dunno; I would much prefer if you would indeed chime in: participation is not an interruption! It would be wonderful to see participation from other LDT members as well, even if it's one person who knows Roslyn well, and even better if he/she can ask other busy team members for their opinion.

@zspitz

This comment has been minimized.

Copy link
Author

@zspitz zspitz commented Dec 19, 2018

@KathleenDollard To add to what bandleader said, I myself have almost no knowledge of how Roslyn works, or even compilers in general; both of which would inevitably preclude some design choices, while enabling others. I am only writing from my day-to-day usage of VB.NET and C#, and a little dabbling in F#. I think that some input from someone with Roslyn experience would be helpful in not barking up the wrong tree, or to confirm that a given design choice is relevant.

@zspitz

This comment has been minimized.

Copy link
Author

@zspitz zspitz commented Dec 19, 2018

(This is a rewrite of the initial required functionality for pattern matching above. Thanks for everyone's help in clarifying this; in particular, the discussions I've had with @bandleader have been extremely illuminating.)

  1. Is cannot be extended, as it's currently used for reference equality; we'll require a dedicated keyword for boolean contexts -- e.g. If <expression> Matches <pattern> Then, Do While <expression> DoesntMatch <pattern>. However, we should extend Case to use Case <pattern> without an additional keyword.

  2. <expression> should be a valid pattern, using value equality where available, and reference equality if not. (This would have the same effect on Case as #119).

  3. Anything valid in Case today should also be a pattern:

    • <pattern>, <pattern> -- OR pattern
    • <expression> To <expression>
    • Like <string expression>
    • [Is] <comparison operator> <expression>
  4. For newcomers to pattern matching who may not be familiar with the full range of potential available patterns, the most obvious patterns are those that relate to type-checking and variable-introduction. The initial release of pattern matching should therefore include these three patterns or pattern variants:

    • variable introduction + typecheck pattern
    • variable introduction pattern
    • typecheck pattern

    Figuring out the syntax for these is a separate issue (#367). But whatever the syntax, it has to read well in recursive patterns as well, even if currently the only recursive pattern is the OR pattern.

  5. Since Case already supports expressions combined with operators, we must consider anything until the end of the condition / Case clause as part of the pattern.

    1. Additional logic cannot be expressed using additional logical operators, as they would be considered part of the pattern. AFAICT we'd have to wrap such logic in a single-per-pattern-expression When clause.
    2. We can't use AndAlso because it is an operator used currently in expressions and wouldn't clearly delineate the end of the pattern itself. When parallels C#'s when and cannot be an operator in an expression (it's valid only in Catch clauses).
  6. Since Case supports arbitrary expressions, any pattern must almost always be invalid as an expression, to allow reliable disambiguation. This is easily done by using some keyword to introduce the pattern, such as Case Of <typename> for a typecheck pattern, or If o Matches Dim x As Integer Then as a variable+typecheck pattern.

  7. Case should continue to not require Is.

  8. Some further questions:

    1. Specifying a non-match -- In a boolean context, Matches could be paralleled by DoesntMatch. Perhaps Case should not allow non-matches? There is precedent for this -- Case Is can currently be used, but not Case IsNot.
    2. How necessary is exhaustiveness at this point? Is it even possible, considering the way VB.NET makes rather fluid shifts between types? I know that currently C# doesn't implement exhaustiveness.
    3. IIUC, VB.NET requires Is because comparing two objects which have a default property would result in both being converted to their respective values via the default property, and value-equal comparing the results; it's then impossible to compare reference equality for the two objects. How would this issue be handled within pattern matching on the expression pattern?
    4. In the DoBottomLoopStatement, should we allow variables introduced with Loop While <expression> Matches <pattern> to bleed back into the body of the Do? Seems very counterintuitive.

And the current state of the grammar:

Where a pattern expression can be used:

BooleanOrPatternExpression
    : BooleanExpression
    | Expression 'Matches' PatternExpression
    ;


// If...Then..ElseIf blocks

BlockIfStatement
    : 'If' BooleanOrPatternExpression 'Then'? StatementTerminator
      Block?
      ElseIfStatement*
      ElseStatement?
      'End' 'If' StatementTerminator
    ;

ElseIfStatement
    : ElseIf BooleanOrPatternExpression 'Then'? StatementTerminator
      Block?
    ;

LineIfThenStatement
    : 'If' BooleanOrPatternExpression 'Then' Statements ( 'Else' Statements )? StatementTerminator
    ;


// Loops

WhileStatement
    : 'While' BooleanOrPatternExpression StatementTerminator
      Block?
      'End' 'While' StatementTerminator
    ;

// introducing variables with Until could only be used 
// by the When clause, not within the block
DoTopLoopStatement
    : 'Do' ( WhileOrUntil BooleanOrPatternExpression )? StatementTerminator
      Block?
      'Loop' StatementTerminator
    ;

// introducing variables with either While or Until could only be used 
// by the When clause, not within the block
DoBottomLoopStatement
    : 'Do' StatementTerminator
      Block?
      'Loop' WhileOrUntil BooleanOrPatternExpression StatementTerminator
    ;

ConditionalExpression
    : 'If' OpenParenthesis BooleanOrPatternExpression Comma Expression Comma Expression CloseParenthesis
    | 'If' OpenParenthesis Expression Comma Expression CloseParenthesis
    ;


// Within a Case clause

CaseStatement
    : 'Case' PatternExpression StatementTerminator
      Block?
    ;

What are the parts of a pattern expression?

PatternExpression
    : Pattern ('When' BooleanExpression)?
    ;

What patterns should be supported from the start?

Pattern
    // patterns with subpatterns
    : Pattern ',' Pattern                    // OR pattern (already supported in Case)

    // patterns without subpatterns
    | 'As' TypeName                          // Type check pattern -- matches when subject is of TypeName
    | 'Dim' Identifier ('As' TypeName)?      // Variable pattern -- introduces a new variable in child scope; as TypeName or Object
    | 'Is'? ComparisonOperator Expression    // Comparison pattern
    | 'Like' StringExpression                // Like pattern
    | Expression 'To' Expression             // Range pattern
    | Expression                             // Expression pattern -- value/reference equality test against Expression
    ;

Pinging @KathleenDollard @ericmutta @paul1956

@KathleenDollard

This comment has been minimized.

Copy link
Contributor

@KathleenDollard KathleenDollard commented Dec 21, 2018

Notes from our meeting on this Wednesday

You all are awesome.

@ericmutta

This comment has been minimized.

Copy link

@ericmutta ericmutta commented Dec 21, 2018

@KathleenDollard thanks for the update from the LDM!

I think this kind of iteration and feedback loop between the community and the LDM is really awesome and should become the way to do things going forward, that is:

  1. LDM shares what they are considering (this is important and helps the community focus on things that have the highest probability of happening before the universe dies).

  2. community rallies around that and talks about it to flesh it out.

  3. then LDM comes back with feedback and own thoughts.

  4. rinse and repeat until something epic happens.

You are all awesome (especially having language design meetings so close to X-Mas!)

@ericmutta

This comment has been minimized.

Copy link

@ericmutta ericmutta commented Dec 22, 2018

@bandleader Another issue with extending Is to work with patterns and supporting an expression as a pattern...

I can see now that using Is could be more trouble than it's worth because Is already has several uses. What about Like though? The only thing this operator has done since day one is pattern matching, which is exactly what we are talking about here!

If obj Like String Then Console.WriteLine("obj is a string")

If obj Like s As String Then Console.WriteLine(s.Length)

Dim test1 = obj Like String
Dim test2 = obj Like s As String

If obj Like s As String When s.Length > 5 Then Console.WriteLine(s.Length)

Select Case obj
  Case Like String
    Console.WriteLine("obj is a string")
  Case Like s As String
    Console.WriteLine(s.Length)
  Case Like s As String When s.Length > 5
    Console.WriteLine(s.Length)
End Select

Do While obj Like s As String
 Process(s)
 obj = GetNextObject()
Loop

Do Until obj Like String
  ProcessObjectsThatAreNotStrings(obj)
Loop

...something to consider before introducing an entirely new keyword and creating a scenario where we have two keywords doing pattern matching 👍 It also seems to negate naturally and do ranges/tuples quite nicely:

If num Like 1 To 10 Then Console.WriteLine("number between 1 to 10")

if num Not Like 1 To 10 Then Console.WriteLine("number NOT between 1 to 10")

if MyTuple Like (String, Integer) Then Console.WriteLine("tuple of string and integer")
@franzalex

This comment has been minimized.

Copy link

@franzalex franzalex commented Dec 22, 2018

@ericmutta
What about Like though?

I can't believe we didn't suggest this earlier! By extending its usage to include pattern matching, Like can be the perfect keyword short of introducing a new one.

@zspitz

This comment has been minimized.

Copy link
Author

@zspitz zspitz commented Dec 22, 2018

@ericmutta @franzalex Note that @AdamSpeight2008 suggested it in #124.

@zspitz

This comment has been minimized.

Copy link
Author

@zspitz zspitz commented Dec 23, 2018

Thanks to the members of the LDT for discussing this, and all your hard work on VB.NET; and for taking into account the community's contribution.

(I'm responding here to the meeting notes; @KathleenDollard if there's a better place to put it please let me know.)


(I'm addressing this first, because it affects some subsequent points.)

It's not clear how variable introduction without typechecking would work, or how type checking without assignment differs from the available TypeOf x Is .

Without recursive patterns, there is indeed no difference. But once recursive patterns are introduced, and the pattern itself doesn't sufficiently enforce the type of the item in question, we may want to enforce a specific shape of parts of the item without having to explicitly name those parts. For example, with the tuple pattern:

Dim o As Object
Select Case o
    Case (Integer, Integer)
        Console.WriteLine("Pair of numbers")
    Case (String, String)
        Console.WriteLine("Pair of strings")
End Select

If every type check also requires variable introduction, we have the needlessly cluttered:

Dim o As Object
Select Case o
    Case (Integer Into x, Integer Into y)
        Console.WriteLine("Pair of numbers")
    Case (String Into s1, String Into s2)
        Console.WriteLine("Pair of strings")
End Select

It's not clear how variable introduction without typechecking would work

With recursive patterns, the converse is also true -- I may want to extract part of the root matched value, without assigning a new name to the root. Using the array literal pattern (#141):

Select Case o
    Case {Into firstArg, String, String} When firstArg = "help" Or firstArg = "version"
        Console.WriteLine($"First argument -- {firstArg}")
    Case Else
        Console.WriteLine("Invalid first argument")
End Function  

Not all of us are happy with the reading of the If syntax. The human English wording would be more like "If o matches string, put it in x as a string."

@bandleader has also pointed out that Case x As String is rather counter-intuitive, because everywhere else in the language, omitting the As String doesn't change the basic meaning of the statement; both the following statements declare a variable, albeit of different types:

Dim x
Dim x As String

The same applies for method parameters:

Sub Foo(bar As String)
Sub Foo(bar) ' defaults to parameter of type Object

and in all other places where a variable is introduced into a child scope -- For, For Each, Using, and Catch.

However, the following two Cases mean very different things:

Case x ' is the case value equal to the already-existing expression `x`?
Case x As String ' `x` doesn't exist; if the case value be assigned to a String, introduce a new `x`

I think Into is an excellent choice, because as noted the variable introduction (Into x) follows from the
typecheck (Case String). In addition, Into already has a similar usage when inline-declaring a variable for a grouped or aggregate LINQ query.

(NB. What happens if there is an identifier String in scope? Would it be better to disambiguate with some keyword: Case Of String Into x or Case As String Into x?)

This will lead to a discussion about whether it is more important to read like English or to look like a declaration here.

I don't think "looking like a declaration" has inherent value. The only reason I can see to prefer Case Dim x As String over Case x As String is in order to visually distinguish from Case x; Case String Into x does this equally well, if not better.

#2. We are a little confused. The effect of #119 seems desirable, but not sure whether this is a pattern or evaluation (or what distinctions matter here).

This was only relevant if 1) using Is as the pattern-match operator in boolean contexts, 2) everything supported by Case is a pattern, 3) and the <expression> pattern would have the same behavior in both Is and Case. Since Is <expression> tests for reference equality, Case <expression> would also test for reference equality, and <expression> would be considered a special case of <pattern>.

Since 1) the LDT has decided on Matches for boolean contexts and, 2) some syntaxes matched by Case will not be patterns, this no longer appears relevant to pattern matching. (It's nice to have independent of pattern matching though.)

#3. We thought about commas...

Agreed.

Certainly everything that works in the context of a Case today should continue to work in a Case. But hesitate on moving syntax from Case to other places patterns can be used.

With recursive patterns, if an expression is considered a special case of pattern, it becomes possible to write something like this (e.g. using the tuple pattern):

Dim o As Object
Dim x = 5
If o Matches (x, x+1, x+2) Then

#4. The linked "full range of potential patterns" is for F# and several of these are not available in other .NET languages.

I didn't mean to imply that all these patterns should be in VB.NET; only that while typecheck+variable is the big draw for those who have never used it, pattern matching is a far more generalized idea than just typecheck+variable. #367 contains a list of patterns that might be of value specifically in VB.NET.

#6. Can we get clarity on this. Is this basically saying there can't be ambiguity and we can't break existing code?

A rather specific ambiguity. For patterns which mean a literal expression in other contexts (e.g. the tuple pattern), since Case supports any expression, it is necessary to distinguish between Case <expression> and Case <pattern>. (C# doesn't have this problem, because initially only constants were supported by switch.)

(This may not be an issue when the type of the resulting literal expression is a value type. For example, even though this is ambiguous:

Dim t = (1, 2)
If t Matches (1,2) Then

between:

  • is it matching against the tuple pattern?
  • Or is it matching against a newly created tuple?

but it doesn't matter; since a ValueTuple is a value type, multiple instances of ValueTuple are the same as long as their members are the same.)

#7. Do you mean Case should not require Is where it does not require it today?

This is about using Case Is <pattern> to distinguish between matching against a pattern, and Case <expression> to check value-equality on an expression. The Is in Case would then be required (for patterns), optional (for comparison operators), or disallowed (for simple expressions), based on context. Really confusing.

#8 iii) Need clarity on what this is saying

I made a mistaken assumption here; it's irrelevant.

// introducing variables with either While or Until could only be used
// by the When clause, not within the block
// LDM thinks probably within the block as well

This was a typo -- when introduced with a While variables should be usable within the block, but when introduced with an Until, variables should not be available within the block.

@zspitz

This comment has been minimized.

Copy link
Author

@zspitz zspitz commented Dec 24, 2018

@KathleenDollard

Emphasizing one additional point:

For background: a pattern is not an expression, but a thing that when matched results in an expression, in this context a Boolean expression.

A pattern may not be an expression, but every expression could be considered a pattern that matches on value-equality (o. With recursive patterns, this would enable using expressions as sub-patterns:

Dim x = 5
Select Case o
    Case (x, x+1)
End Select
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants
You can’t perform that action at this time.