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: Conjunctive and Disjunctive Patterns #6235

Closed
alrz opened this issue Oct 22, 2015 · 29 comments
Closed

Proposal: Conjunctive and Disjunctive Patterns #6235

alrz opened this issue Oct 22, 2015 · 29 comments

Comments

@alrz
Copy link
Member

alrz commented Oct 22, 2015

(Extracted from #5402 as it was orthogonal to the subject)

The following is based on the syntax proposed in #206:

conjunctive-pattern:
complex-pattern
conjunctive-pattern and complex-pattern

disjunctive-pattern:
conjunctive-pattern
disjunctive-pattern or conjunctive-pattern

pattern:
simple-pattern
disjunctive-pattern

A conjunctive-pattern matches the pattern input against two patterns, if either failed, pattern fails.

In a disjunctive-pattern, the pattern input is matched against the first pattern. If that fails, the pattern input is matched against the second pattern. Both patterns must bind the same set of variables with the same types.

EDIT: For OR pattern we can use #1419 rules to select the most common type between both sides.

@HaloFour
Copy link

I really like the idea but don't really care for the syntax. I'd prefer that the standard && and || operators be used rather than adding new keywords and I think that the existing operators would be intuitive and familiar to current C# developers.

"Conjunctive" patterns seem like they should be relatively easy by themselves since each pattern would only introduce its own variables and there is no concern about pattern precedence.

"Disjunctive" patterns add the additional requirement to the compiler to ensure that the variables would all be definitely assigned and inferred (if not explicitly set) to the same type. I'm not saying that's a problem and you do already mention it, just reiterating the point. I also think that since logical or operations have a lower precedence than logical and operations that you'd want to introduce parenthesis in order to allow the developer to force order of operations.

var result = match obj {
    // matched if A is matched and if either B or C is also matched
    case A(var x) && (B(var y) || C(var y)): x * y,
    // matched if both B and C are matched, or if just A is matched
    case A(*) && B(*) || C(*): x + y
    ...
};

@HaloFour
Copy link

To note, I think C# developers are really going to expect this kind of functionality especially since they can use pattern matching directly in if statements.

if (obj is A(var x) && (obj is B(var y) || obj is C(var y))) {
    Console.WriteLine($"( {x}, {y} )");
}

@gafter
Copy link
Member

gafter commented Oct 22, 2015

@HaloFour error: the name 'y' is already used to declare a variable in this scope

@alrz
Copy link
Member Author

alrz commented Oct 22, 2015

@HaloFour I'm not sure about using logical and/or operators in this context. Since logical operators themselves are not allowed in the patterns, this would be confusing (or even ambiguous when used with is operator). Parentheses would be a good addition, and together with this syntax there would be no reason to repeat a pattern.

@HaloFour
Copy link

@gafter

That it would. But if you changed the second y to a z then you would not be permitted to use either identifier as neither could be proven to be definitely assigned.

I wouldn't argue for altering that behavior in normal Boolean expressions. However I do think that pattern matching with switch and match would be quite handicapped if it didn't have support for both "OR" patterns as well as permitting reuse of the identifier where the compiler could prove that the variable could only receive a value from one of the patterns. Notably F# already supports this with "OR" patterns and requires that both sides bind the same set of variables:

let result =
    match point with
    | (0, x) | (5, x) -> x
    | (10, x) | (15, y) -> x // ERROR: The two sides of this 'or' pattern bind different sets of variables
    | _ -> 0

@alrz Maybe not, but it seems weird to have two ways to express Boolean logic and they apply to completely different places. Can't say I care for unless either, evaluating backwards lexically is very unintuitive.

@alrz
Copy link
Member Author

alrz commented Oct 22, 2015

Worth to mention, the syntax of match .. with in F# makes multiple-cases totally transparent with "or" patterns (using a | for both), while for the sake of readability I think that should be possible in match; by the way, it uses -> for lambda expressions too and allows to use patterns directly in function parameters which make all these syntactically similar.

@alrz
Copy link
Member Author

alrz commented Oct 22, 2015

@HaloFour That doesn't make sense, the whole expression containing or is a pattern and compiler will know that it should check for variables, while obj is B(var y) || obj is C(var y) is a general expression, how should compiler know about that?

@HaloFour
Copy link

@alrz Which is why I mentioned only worrying about that in switch and match given that normal Boolean expressions don't apply there. But I admit that it would be confusing in its own right that the behavior couldn't be the same. Different operators may make more sense. F#'s | is specific to pattern matching, using || for logical operations and ||| for bitwise operations. That's not to say that I'm a fan of specifically using and and or as these operations, but I don't know what would feel more like it derives from the heritage of C/C++ (not that it necessarily has to).

@gafter
Copy link
Member

gafter commented Oct 22, 2015

Patterns may contain (almost general) expressions (as long as they resolve to a constant). Generalizing patterns to look like expressions may introduce an inherent ambiguity.

@alrz
Copy link
Member Author

alrz commented Oct 22, 2015

@gafter

Patterns may contain (almost general) expressions (as long as they resolve to a constant).

Irrelevant, but will this be supported?

// assuming
public static Mult operator *(Expr a, Expr b) => Mult(a, b);
// then
expr match( case Expr a * Expr b : ... ) 
// instead of
expr match( case Mult( a , b ) : ... ) 

@gafter
Copy link
Member

gafter commented Oct 22, 2015

@alrz That doesn't even syntactically look like a pattern (or an expression); how could it work?

@alrz
Copy link
Member Author

alrz commented Oct 22, 2015

@gafter I take it as a no then.

@gafter
Copy link
Member

gafter commented Oct 22, 2015

@alrz As you wish, "no" it is. 😉

@alrz
Copy link
Member Author

alrz commented Oct 22, 2015

@gafter FYI, it is a pattern in Mathematica.

@gafter
Copy link
Member

gafter commented Oct 23, 2015

Really? And how does Mathematica associate the implementation of your overloaded operator * with the way the pattern works?

@alrz
Copy link
Member Author

alrz commented Oct 23, 2015

Really. Well it hasn't operator overloading, Everything is a function: + is Plus etc. I'm not really familiar with the type system but something like f[n_ c_] := ... is permitted.

@HaloFour
Copy link

Actually, I mistranslated my pattern as an expression (of a pattern). Instead of:

if (obj is A(var x) && (obj is B(var y) || obj is C(var y))) {
    Console.WriteLine($"( {x}, {y} )");
}

it should be:

if (obj is (A(var x) && (B(var y) || C(var y)))) {
    Console.WriteLine($"( {x}, {y} )");
}

I am using the && and || operators to represent the "conjunctive" and "disjunctive" patterns respectively but don't focus on the operators themselves. I think that in this form that the compiler could make the same guarantees that the type of y in all locations is compatible and that it would be definitively assigned exactly once, permitting it to both be "reused" as well as usable within the then-stmt.

@alrz
Copy link
Member Author

alrz commented Oct 23, 2015

You are using type intersection (#4586) I presume, I think that is a totally new kind of type and now you're introducing it as a pattern. I have no idea how this would work.

PS: Sorry I think that is not. That's because you have suggested && also for type intersections.

@HaloFour
Copy link

@alrz Not at all, they could be any custom patterns that are in scope that can be applied to obj. You could replace them with any pattern that can take a simple-pattern as a subpattern.

@alrz
Copy link
Member Author

alrz commented Nov 12, 2015

I think constant-pattern should be part of complex-pattern so that value is 2 or 10 would be possible. I don't know why it is changed in the spec. Probably because of the let?

PS: If disjunction is not planned currently, still, match-expression should support multiple-cases to be closer to its statement form — in that case presence of comma would be even more confusing.

@gafter
Copy link
Member

gafter commented Nov 12, 2015

@HaloFour

it should be:

if (obj is (A(var x) && (B(var y) || C(var y)))) {
    Console.WriteLine($"( {x}, {y} )");
}

The thing to the right of your is does not appear to be any proposed syntax form for a pattern. To begin with, the only pattern form that could start with an open paren is a constant pattern (eventually perhaps the tuple pattern).

@gafter
Copy link
Member

gafter commented Nov 12, 2015

@alrz Not sure why value is 2 is an important use case.

@alrz
Copy link
Member Author

alrz commented Nov 12, 2015

@gafter I said value is 2 or 10, instead of value == 2 || value == 10 (#136).

@HaloFour
Copy link

@gafter You're correct. I don't know what syntax would work the best. Either way it's the concept in which I'm more interested, the ability to apply Boolean operators to patterns supporting operator precedence as well as support for applying the same identifier to multiple variable patterns where it can be guaranteed by the compiler that it would result in exactly one assignment.

Effectively, everything F# does. 😄

@alrz
Copy link
Member Author

alrz commented Nov 12, 2015

@HaloFour I don't think that even F# supports parens in patterns in order to change the precedence.

@HaloFour
Copy link

@alrz I think you're right. The docs do demonstrate combining AND and OR patterns.

@HaloFour
Copy link

@alrz Actually, rereading F#'s supported patterns it does have "Parenthesized Pattern" which is specifically for defining the associativity and precedence of the patterns:

let countValues list value =
    let rec checkList list acc =
       match list with
       | (elem1 & head) :: tail when elem1 = value -> checkList tail (acc + 1)
       | head :: tail -> checkList tail acc
       | [] -> acc
    checkList list 0

let result = countValues [ for x in -10..10 -> x*x - 4 ] 0
printfn "%d" result

@alrz
Copy link
Member Author

alrz commented Nov 20, 2015

@HaloFour Neat. I'm not against it anyway.

@alrz
Copy link
Member Author

alrz commented Mar 21, 2017

Moved to dotnet/csharplang#118

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

3 participants