-
Notifications
You must be signed in to change notification settings - Fork 66
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
Pattern Matching #124
Comments
Another possible option is to extend the usage cases of |
Pattern matching is absolutely a VB.NET feature, especially considering it already exists in limited form as part of I would suggest that any pattern matching syntax should have the following features:
Proposed spec (using bits from the ANTLR-like grammer available from here): PatternExpression
: Pattern ('When' BooleanExpression)?
;
Pattern
// patterns with subpatterns
: Pattern ',' Pattern // OR pattern
| '(' Pattern (',' Pattern)* ')' // Tuple pattern
| 'Not' Pattern // NOT pattern
// patterns without subpatterns
| '*' // Matches any value, including Nothing
| Literal // Matches a literal value
| Identifier // Existing Identifier pattern -- when identifier already exists in scope; tests for value/reference equality
| 'Of'? TypeName // Type check pattern -- when type name already exists in scope
| Identifier ('As' TypeName)? // Variable pattern; introduces a new variable in child scope
// The type of the variable (if not specified) is the same as the subject of the pattern match
| 'Is'? ComparisonOperator Expression // Comparison pattern
| 'Like' StringExpression // Like pattern
| Expression 'To' Expression // Range pattern
| Expression // Equality pattern -- value/reference equality test against Expression
;
// usage in Select Case` looks like this:
// replaces existing CaseStatement
CaseStatement
: 'Case' PatternExpression StatementTerminator
Block?
; and theoretically, it could be used anywhere BooleanOrPatternExpression
: BooleanExpression
| 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
;
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
; Some additional points:
Related issues: #307 , #191 -- Pattern matching as a Specific patterns: #141, #140, #139 Pinging @KathleenDollard @AdamSpeight2008 @bandleader @gafter @AnthonyDGreen @ericmutta |
An alternative spec for
Using this syntax would mean that subpatterns would also be forced to introduce new variables in the same way: Dim o As Object
'...
Dim flag = o Matches (x As Integer, y As String) When x > 15 And y.Length < 7
If o Matches x As Random, y As Dictionary(Of String, Integer) Then
If x IsNot Nothing Then Console.WriteLine($"Next number: {x.Next}")
If y IsNot Nothing Then Console.WriteLine(y("Key") * 12)
End If |
What would the inverse of this look like? If Not value Matches 0 To 3, 99 Then return "Valid" Else Return "Invalid"
' or
If value DoesNotMatch 0 To 3, 99 Then return "Valid" Else Return "Invalid"
' or
If value DoesntMatch 0 To 3, 99 Then return "Valid" Else Return "Invalid"
' or
If value IsNotMatch 0 To 3, 99 Then return "Valid" Else Return "Invalid" |
Instead of a new IsExpression
: Expression 'Is' LineTerminator? Expression
| Expression 'IsNot' LineTerminator? Expression
; and we could extend it to any pattern: IsExpression
: Expression 'Is' LineTerminator? PatternExpression
| Expression 'IsNot' LineTerminator? PatternExpression
; |
|
Doesn't every spec change have the potential to similarly break existing code analyzers? Also, are you proposing that Dim rnd As New System.Random
If o Is rnd Then ... is currently supported, so too this: If o Is (rnd, 15) Then ... should also be supported.
My mental model for
I think that just as is in natural language isn't constrained to a particular form on the RHS, so too in VB.NET |
@zspitz I'm with you on supporting expressions (and I like Spot). I just seem to remember some opposition due to conflicts. If resolved via a keyword (do you mean something like As far as each spec change breaking code analyzers -- no, it's only if the structure of the AST changes. We can add new keywords, or even new AST node types; we avoid, however, causing an existing piece of code (i.e. |
@bandleader My initial thought (described here) was to resolve conflicts based on context:
but I suspect that is too over-clever for VB.NET. |
@zspitz That should indeed solve the problem -- since 1 will fire before 2, it won't break code. (Though the first bullet point has to be done without wrapping the expression in a PatternExpression, so as not to break analyzers.) But you're right that it might be "too clever for VB.NET" -- i.e. isn't really VB-like or VB-friendly -- doesn't really fit the VB requirements for "seeing the code is seeing what it does." I could see the VB team opposing it on two counts:
This way, the intent and behavior is evident from the pattern itself, and doesn't depend on context. (Also, when the intent is to match on the value of an existing variable, it prevents mistyping the variable name, and also allows us to provide Intellisense for the name.) |
As per my above comment, I do prefer your "context-free" suggestion (as described here) as fitting much better into VB, where it's important to us that intent be reflected in words, and "seeing the code is seeing what it does." However, I would change the following:
|
Another possibility for the type-checking pattern might be
Might be related, but |
This is only true if we're relying on context to differentiate between the expression pattern and the type-check pattern (if the single-identifier expression matches an identifier in context, then it's an expression pattern; otherwise a type-check pattern). |
Since IMHO the |
I'd personally like pattern matching feature like this: Dim obj As Object = "Test"
If TypeOf obj Is String str AndAlso str.Length > 0 Then
' Stuff
End If
' For select statement
Select TypeOf obj ' Or Select Case TypeOf obj
Case Integer int
' Do stuff with int
Case String str
' Do stuff with str
End Select
' This would be an equivalent of
Select Case True
Case Integer.TryParse(obj, Out int)
Case String.TryParse(obj, Out str)
End Select
' Or
Select Case True
Case TypeOf obj Is Integer
Dim int = DirectCast(obj, Integer)
Case TypeOf obj Is String
Dim str = DirectCast(obj, String)
End Select |
@JustNrik Your syntax limits pattern matching to type-check + cast-to-a-new-variable (which could be covered by #172, without introducing a new variable). It's important to realize that pattern matching in other languages is a far broader concept -- does the left-hand-side fit into the pattern described on the right-hand-side, with some parts of the pattern being named: Select obj
Case int As Integer
' Do stuff with int
Case (int As Integer, s As String)
' Do stuff with int or s
End Select |
@zspitz I hear you; you're viewing pattern matching how it's viewed in other languages (Scala etc.): "Does the LHS match the pattern on the RHS, where the RHS can also have parts which match all values (optionally limited by type) and just assign it to a variable." If what we're looking to do is extend Select Case, then we shouldn't change the meaning/context of Doing Scala-style pattern matching in VBThat said, I have no problem with switching to the Scala idea of pattern matching, but in that case we should not re-use Also, somebody suggested that pattern matching should be implemented as an operator (expression-based) as opposed to a control statement that runs other statements. I personally would like this a lot. Reduces a lot of boilerplate in which you set (e.g.) Dim message = "You have " & emailCount Match(
0: "no new email."
1: "one new email."
x As Integer When x > 1: $"{x} new emails."
Else: "a broken mailbox."
) & " Click the icon for details." I love this, but I'm not sure how the average "bus-driver programmer" would take to it. Versus the old way -- less powerful, and much less DRY -- not just longer, but also you have to change in 4 places if decide not to Dim message = "You have "
Select Case emailCount
Case 0: message &= "no new email."
Case 1: message &= "one new email."
Case Is > 1: message &= $"{x} new emails."
'Testing for Integer type can't currently be done here at all.
Case Else: message &= "a broken mailbox."
End Select
message &= "Click the icon for details." |
Only because in VB.NET there are currently no semantics for introducing a new variable in the first line of a construct. There's nothing similar to using C#'s if (int.TryParse('1234", out var x)) {
// x is an int here
} On second thought, there are actually two such constructs, and neither uses Sub Foo(x As Integer)
...
End Sub and lambda expressions / statements: Dim Bar = Sub(x As Integer)
End Sub From the discussion at #177 (and particularly here) it seems that |
@zspitz No, I mean because existing behavior of |
The existing behavior of the Dim x = 5, y = 10
Dim n = 5
Select Case n
Case x
Console.WriteLine("Exact match")
Case x, y
Console.WriteLine("Either, or")
Case x To y
Console.WriteLine("Between")
End Select I don't think we need to enforce that every pattern not start like the simple value match of So now the question becomes, what syntax is appropriate for a pattern that introduces a new variable in a child scope, We have two existing syntaxes for introducing identifiers:
I think the semantics of a "variable-introducing pattern" are much closer to 2. |
Also, don't forget that On another note, just using However, there is one syntax already part of Select Case obj
Case foo
Case Nothing
Case Is > 32
Case Is s As String
Case Is (> 0, y As Double) 'Tuple pattern with nested patterns
Case Is bar When z > 0 'Declare bar with type inference
Case Else
End Select |
@gilfusion I can't believe I forgot those.
I'm OK with that limitation (actually I think this limitation is required -- see end of this post), and most of the patterns I've described here are actually not currently valid expressions, which means they can easily be differentiated from The one exception is the tuple pattern (
I'm hesitant to use
which I think would be very confusing. Moreover, your suggestion of Dim a As New List(Of String)
Dim x As Integer
Select Case n
Case a(x) ' Checks whether n is equivalent to the value at position x in the list
Case Is a(x) ' Checks whether n is an object with a default property
End Select But what happens here? Select Case n
Case Is (a(x)) ' Is this trying to match a 1-tuple with the value at position x?
' Or is it trying to match a 1-tuple with an object with a default property?
End Select I think the only way around this is to force all patterns to be invalid as expressions (unless the pattern is either matching against a value type, or matching against an expression), thus differentiating them from patterns. |
@gilfusion I've already suggested an algorithm for differentiating between patterns and expressions, but @bandleader says -- and I agree -- that this is too confusing, and it is better to have all non-expression patterns be umambiguously non-expressions. |
What about a pattern that matches values/patterns against public properties or fields?
When used with the type-check pattern or the variable pattern, Intellisense on the available members could be provided. But this could also be useful for objects whose shape is not known at compile time, such as deserialized JSON. Dim o As Object
Select Case o
Case p As Person With {.Age Matches 18 To 25}
Case p1 As Person With {.LastName Matches Like "A*"}
Case With {.FirstName Of String}, With {.BirthDate Of Date} ' matches only if property exists and is of the appropriate type
End Select Admittedly, this has some overlap with the
ping @bandleader. |
I want to note that after consideration (and some private discussion with bandleader) I agree with this. |
Some people have suggested to me that pattern matching doesn't feel like a VB feature. What they mean is "Pattern matching sounds like some advanced programming stuff". I disagree. I think pattern matching is very much so a VB feature. In fact, I suggest that most VB programmers, myself included, have spent most of their careers looking at data--be it DB records, or string input, or messages from one device or another--and testing the shape of that data against known patterns, separating structure from content, and extracting select content to fit our needs. That's what every use of the Like operator, the InStr function, and most
If
' andSelect
statements are really doing. They're just going about it in a rather tedious (imperative) way. Pattern Matching is a powerful feature we can add to VB to make the same kinds of tasks we've always been doing much more concise, readable, and declarative.I propose we extend existing
Case
statements to support a new kind of clause, theMatch
orMatches
clause (depending on your grammatical proclivities) of the form:Case Match
pattern [When
expression]Where pattern is one of:
The wildcard pattern
*
Matches all values, including null.
The variable pattern
identifier [
?
] [As
typename]?
modifier is used will match null values but will still fail if the value is a non-null value of another type than specified.?
modifier is used.The function pattern
expression
(
[ pattern1 [,
pattern2, ...] ])
Example:
Order of execution in the above example is as follows:
Integer.TryParse
is called passing the value of theSelect Case
statement in as its first argument, a new variable named 'value' of the type of the second argument of theTryParse
method is passed in as the second argument (which is ByRef).TryParse
returns true, execution passes to theWhen
clause. The expressionvalue > 0
is evaluated as true.Case
block.The tuple pattern
(
pattern1 [,
pattern2, ...])
However, the list doesn't end here. In fact, it's my belief that the tuple pattern is just the first example of a new trend in the language that with each new expression we add for constructing an object, there is a corresponding pattern for decomposing that object that mirrors the construction syntax. For example, one can imagine (and I have) patterns based on decomposing arrays based on the array literal syntax, decomposing strings based on the string interpolation syntax, and decomposing XML fragments based on the XML literal syntax. The JSON literal proposal (#101) also demonstrates this.
The entire
Match
(orMatches
) clause succeeds if the specified pattern succeeds AND the expression provided in theWhen
clause (if provided) evaluates as true. The expression may refer to variables introduced in pattern.The text was updated successfully, but these errors were encountered: