-
Notifications
You must be signed in to change notification settings - Fork 4.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
Proposal: Add completeness checking to pattern matching draft specification #188
Comments
This can be another option as well (based on Design 2):
So case classes can only inherit from other case classes. Also, this helps to distinguish between case classes and regular ones, if they were defined in different files. |
@alrz I don't get it. Would it be an error to extend C_1 in another assembly? Or is there some other rule that would allow the compiler to know that it sees all of the cases? |
@gafter Quoting yourself, "It's a closed hierarchy of types" (like discriminated unions in F# but more flexible since you can inherit from other cases) so why should it be extendable in another assembly? |
@alrz so |
@gafter Same keyword is used in Scala for exactly this purpose — implying that each case class corresponds to a case statement, I guess. |
@alrz no, |
@gafter Yes, sealed abstract class C
case object C_1 extends C
case object C_2 extends C and the proposed one: sealed abstract class C {}
class C_1 : C {}
class C_2 : C {} I'm trying to say that I think this is a more expressive syntax compared to above examples, for this specific use case: abstract case class C {} // or sealed abstract case class?
case class C_1 : C {}
case class C_2 : C {} Marking all classes with a unified keyword, indicating that these classes belong to a closed hierarchy. PS: Although, this is just another option to consider. Except for this little concern, |
Will this support multiple levels of inheritance? abstract sealed class C {}
abstract sealed class D : C {}
public class C_1 : C {}
public class C_2 : C {}
public class D_1 : D {}
public class D_2 : D {} So now the compiler will look for either C, (C_1 + C_2 + D) or (C_1 + C_2 + D_1 + D_2) when checking for completeness. |
@orthoxerox Yes, we do not plan to make one level of inheritance any more special that a second level of inheritance. The compiler will have to build a decision tree and ensure that every path reachable from the root is handled. |
@gafter great, I missed that in F#. |
See the excellent comparison of difference languages by @jonschoning in #5154 (comment) |
Would C#7/8 promote having more of multiple types per file or the IDE will be capable of grouping them automatically? |
As I understand this proposal, it would allow the creation of discriminated unions, such as
And, because the compiler knows this is a complete hierarchy, I could pattern match as follows:
In other words, the Have I understood this correctly? |
@DavidArno Yes, that is precisely correct. |
Is this functionality implemented in any of the feature branches yet? |
I suggest void Process(int | List<int> v)
{
switch (v)
{
case int i:
ProcessInt(I);
break;
case List<int> l:
ProcessList(l);
}
} And to make // For structs
struct Type = int | bool;
// For classes or both structs and classes
class Type = int | List<int>; Roslyn may enforce a user to write |
@KalitaAlexey I don't think you can all that ADT, it's more like union type. In ADTs, the cases have names and can be recursive (I can't tell if your proposal would allow that or not). |
I like how it is done in Rust. I think we could inherit their enum ADT. data Expression = Number Int
| Add Expression Expression
| Minus Expression
| Mult Expression Expression
| Divide Expression Expression I'd like to have In C# enum Expression {
Number(int Number),
Add(Expression LeftOperand, Expression RightOperand),
Minus(Expression Expression),
Mult(Expression LeftOperand, Expression RightOperand),
Divide(Expression LeftOperand, Expression RightOperand),
} And pattern matching like void ProcessExpression(Expression expression)
{
switch (expression)
{
case Expression::Number n:
ProcessNumber(n);
break;
case Expression::Add a:
ProcessAdd(a);
break;
case Expression::Minus m:
ProcessMinus(m);
break;
case Expression::Mult m:
ProcessMult(m);
break;
}
case Expression::Divide d:
ProcessDivide(d);
break;
} |
@HaloFour Thanks. What's the difference then? |
The discussion around this concept is scattered everywhere, so forgive me if I just repeat something said elsewhere. Option 1 has serious problems but is on the right track. Option 2 I don't care for because it scatters the declarations and muddles the concept. What we're modeling here is a discriminated union. @KalitaAlexey gets close to the syntax I'd prefer, but just using "enum" is at least confusing, if it doesn't actually cause parsing problem. I'd suggest (and saw others do so as well in other threads) "enum class". public enum class Expression {
Number(int Number),
Add(Expression LeftOperand, Expression RightOperand),
Minus(Expression Expression),
Mult(Expression LeftOperand, Expression RightOperand),
Divide(Expression LeftOperand, Expression RightOperand),
} There's still lots of open questions after deciding on rough syntax like this, however. For instance, in this thread it was suggested a DU could have a type that's also a DU. I'm not sure that makes sense and would suggest not allowing that, knowing you can always add this feature in the future if that turns out to be the wrong decision but you can't remove a feature, ever. I just don't know when such a feature would actually be useful, and without a compelling use case it seems best to err on the conservative side. I've seen other posts where the syntax is very similar to the above but "abstract sealed" is used instead of "enum class". Frankly, I think "abstract sealed" is highly confusing and gives no indication that one is building a DU, while "enum class" is intuitive. |
Issue moved to dotnet/csharplang #486 via ZenHub |
Background
As noted in issue #180, many modern programming programs are data-focused, especially distributed applications which tend to store, manipulate, and move sets of data between different storage and computation points. One solution proposed to deal with this issue is a combination of records and pattern matching. Records provide a simple way to declare and structure the data types and pattern matching provides a way to destructure and manipulate the data.
Problem
Records provide a great way to represent the data and pattern matching provides a great way to manipulate the data, but there is currently no mechanism in the #180 proposal to ensure that the data and the logic remain in sync. The nature of records and pattern matching is that the data declaration code is often far from the data consumption code. In a distributed system it's even more likely that a single data structure will be consumed and manipulated in various parts of the code base. If the data structure is ever modified, there is currently no mechanism in the draft to alert the programmer that all instances of manipulation logic must be updated.
Solution
Add completeness checking to certain
switch
statements on certain record types. The core of this proposal is to provide a warning when aswitch
statement does not handle every possible match on a type hierarchy. This proposal features two possible designs for this idea, presented in order of increasingly intrusive modification to the language.Design 1
This design actually features no new syntax or semantics beyond that of proposal #180. The suggestion is to create a C# type heirarchy which can be guaranteed 'complete' with existing language features. In this case, complete means that it is not possible for a new subclass of the root member of the type hierarchy, so the compiler can be sure that any and all subclasses of the chosen switching type are visible in the current compilation.
We can construct this type hierarchy in existing C# with the following rules:
Here's an example of the structure of this type hierarchy:
This guarantees that
switch
ing on an instance of typeC
which explicitly matchesC_1
...C_n
has matched against every possible instance ofC
. The only thing which changes about the language specification is a requirement that the compiler produce a warning when not all cases are matched.Design 2
There are a few disadvantages to Design 1:
sealed
or adding any public constructors won't produce a compiler error or warning, but the compiler will now silently skip the completeness check.sealed
orprivate
markers are mostly part of the 'incantation' of completeness and are not directly related to the task at hand.Design 2 attempts to fix these problems by replacing much of the boiler plate with a new combination of modifiers on a type --
abstract
+sealed
. Under Design 2, marking the root type of a hierarchy asabstract sealed
will cause the structure from Design 1 to be generated by the compiler in lowering. The following example demonstrates what the structure from Design 1 looks like with anabstract sealed
type:In this case, most of the problems with Design 1 are solved, but new semantics are required to be added to the language.
The text was updated successfully, but these errors were encountered: