-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Treatment of positional (e.g. tuple) patterns vs parenthesized expressions #82
Comments
As mentioned in roslyn repo before: what if we require a trailing comma to disambiguate a oneple? |
@alrz The point of this issue is to ask the question: "what if we do not require a trailing comma" (or any other extra syntactic noise just to satisfy the compiler) |
The meaning of
I tentatively like 3 a lot. I think this is feature discussion. I'll reclassify as such, and @gafter if you disagree, it goes to show that we need to firm up classification! 😄 |
I'm loathe to introduce anything that gives credence to oneples. |
@MadsTorgersen Using the type explicitly is already required for the runtime-type tests that don't use This is a spec bug because the current spec for pattern-matching is ambiguous in this case. |
Here is the grammar exhibiting the ambiguity constant_pattern
: shift_expression
;
positional_pattern
: type? '(' subpattern_list? ')'
;
subpattern_list
: subpattern
| subpattern ',' subpattern_list
; The pattern One thing we could consider doing is disallowing a simple parenthesized expression to be used as a constant pattern. It would always be interpreted as a positional pattern. |
If we ever want to support parenthesized patterns (possibly along with some pattern operators like dotnet/roslyn#16766 or dotnet/roslyn#6235) to specify precedence, we probably should be avoiding any special meaning for the |
That means a pair of parentheses dramatically changes the meaning of code, conditionally? IMO wrapping parentheses should be always insignificant, for expressions, patterns or even types, just like |
Should we allow to deconstruct values straight into tuples during pattern matching at all? I'd rather write the type name every time. Well, I can see the aesthetic value of omitting it for simple wrapper types, like Slope slope = GetSlope();
switch (slope) {
case Slope(double.PositiveInfinity):
case Slope(double.NegativeInfinity):
//blah
break;
case Slope(var d):
//do something with d
break;
}
//vs
Slope slope = GetSlope();
switch (slope) {
case (double.PositiveInfinity):
case (double.NegativeInfinity):
//blah
break;
case (var d):
//do something with d
break;
} but I'd rather have an explicit token you can F12 and go to the definition. I nearly proposed extending option 3 to standalone unparenthesized expressions, but that would make |
@orthoxerox I recall that it's explicitly captured in a design note before that the LDT likes to be able to omit the type if it's statically known, that would be also applied to property-patterns e.g. |
Then I guess you could force every type to implement an implicit conversion that matches the single-out-parameter deconstructor. |
@orthoxerox I haven't checked the spec but I think implicit conversions do not contribute to that rule. |
I am of the opinion that the compiler should treat class Price
{
public static implicit operator int(Price price) => price.Cents;
}
// later
var p = new Price(...);
switch (p) {
case (3): // perfectly legal C# 7.0
break;
} Given such my opinion is that when the compiler knows the type in question that it attempts to resolve an accessible implicit operator to a compatible target type. If one doesn't exist, then it falls back by trying to resolve an accessible However, that does beg the question as to whether the compiler should follow the same logic when it doesn't know the type. Should it also attempt to resolve a compatible implicit operator and fallback to a object p = new Price(...);
switch (p) {
case Price(3): // potentially legal C#next given the definition of Price above
}
// equivalent to:
switch (p) {
case Price temp when temp == 3:
} |
I disagree. That statement is too broad. The presence or absence of a pair of parentheses can and should drastically change meanings. |
@jnm2 I was talking about the case mentioned in the OP: when there is a single element inside parens. |
So, the compiler will generate code that will call This: switch (p)
{
case (3, 4): ...
} could be, somehow, equivalent to this: switch (p)
{
case p.Deconstruct(out var t1, out var t2) when t1 == 3 && t2 == 4: ...
} Which I would prefer not to use, but that would need to use for oneples and nonples switch (p)
{
case p.Deconstruct(out var t1) when t1 == 3: ...
} |
@paulomorgado That looks like #277 |
I just tried hard to create a womple by invoking the corresponding class MyClass {
public void Deconstruct(out int a) => a = 4;
}
var myClass = new MyClass();
(int a) = inti; // Error CS1525 Invalid expression term 'int' ConsoleApp7
ValueTuple<int> vti;
vti = inti; // Error CS0029 Cannot implicitly convert type 'ConsoleApp7.IsNotTest<int>' to '(int)' With 'twoples' the first one works however. That's why I would go with disallowing womples in pattern scenarios. |
@lachbaer So you're suggesting we should disallow data types that have a single underlying datum? I don't think that makes sense. Units (e.g. Kilograms, Dollars, etc) are often expressed that way, and would be hardly usable with pattern-matching with your suggestion. |
@gafter No, surely I'm not suggesting that 😆 I just think that when the resolution of this one item tuple in a pattern is done by calling an appropriate Deconstruct method I wonder why that is not possible by the same syntax in other expressions, and then I would disallow that syntax form. Does it make sense to have an |
And besides that would allow for same type |
While it might or not make sense, it would not be particularly useful. I'm more interested in the case where the underlying type is different. Using the record-declaration syntax, something like class Dollars(decimal Value); |
Would the ambiguity disappear if a parenthesized expression was a oneple? |
@erikhermansson79 A oneple would not be a constant expression, and therefore would not be a valid constant pattern. In any case, there is no syntax for writing one. |
Our tentative resolution of this is based on @MadsTorgersen's proposed option (3). In summary: There is an ambiguity between a constant pattern expressed using a parenthesized expression, and a “positional” (Deconstruct) recursive pattern with one element:
Here are the two possible meanings:
and
While it is possible for the programmer to disambiguate (e.g. It does this recursively, so this will work too:
A semantic ambiguity arises between these two interpretations when the switched type is, for example, |
And while we're at it, the following will need to go in the spec too: There is a syntactic ambiguity between a cast expression and a positional pattern. The switch case
could be either a cast (i.e. existing code, if A is the name of an enum type,
or a single-element deconstruction pattern with the constant subpattern A being named B.
When such an ambiguity occurs, it will be parsed as a cast expression (for compatibility with existing code). To disambiguate, you can add an empty property pattern part:
|
@gafter: I see. I was thinking more in the line of replacing parenthesized expressions in the language with oneple literals. Since they don't yet exist in the language, maybe they could be added to have the same semantics as parenthesized expressions. Then "case 3" would be a constant expression and "case (3)" a positional pattern with a oneple. |
@erikhermansson79 Tuples are generic struct types named |
Discussion moved to #1054 |
I'm afraid we may have to take a breaking change in the handling of constant patterns when we begin to support positional patterns such as tuple patterns.
Recall that we are treating a type such as
as being capable of being deconstructed
And similarly we will want to support such types in pattern-matching
However, for the purposes of pattern-matching we also want to support types that decompose into zero or one value. An ambiguity arises for decomposition to one value:
Now when we write
Here, it appears to the compiler that we are trying to match an
int
of value3
. Since a value of typePrice
is never anint
of value3
, this fails.Of course, we want it to work. They question is how? Should
case 3:
also work the same way ascase (3):
?The text was updated successfully, but these errors were encountered: