|
| 1 | +# Discriminated Unions |
| 2 | + |
| 3 | +Sundew.DiscriminatedUnions implement discriminated unions for C#, until a future version of C# provides it out of the box. |
| 4 | +The idea is that this package can be deleted once unions are supported in C#, without requiring changes to switch expressions and statements. |
| 5 | + |
| 6 | +In addition, the project supports dimensional unions through default interface methods (traits). |
| 7 | +A dimensional union is a union where cases can be reused in any number of unions, by supporting interface unions through the possibility of implementing multiple interface and default interface members. |
| 8 | + |
| 9 | +## How it works |
| 10 | +A Roslyn analyzer asserts and report errors in case switch statements or switch expression do not handle all cases. |
| 11 | +C# 8 and 9 already comes with great pattern matching support for evaluation. |
| 12 | + |
| 13 | +In order that the inheritance hierarchy remain closed (All cases in the same assembly), an analyzer ensures that unions are not derived from in referencing assemblies. |
| 14 | +Similarly all case classes should be sealed. |
| 15 | + |
| 16 | +Create a union by inheriting from an abstract base (record) class (or interface) marked with the DiscriminatedUnion attribute to build various cases. |
| 17 | +Either specify the partial keyword to the union for a source generator to implement factory methods or use the codefix PDU0001 to generate them. |
| 18 | + |
| 19 | +## Sample |
| 20 | +### Defining a union |
| 21 | +```csharp |
| 22 | +[Sundew.DiscriminatedUnions.DiscriminatedUnion] |
| 23 | +public abstract partial record Result |
| 24 | +{ |
| 25 | + public sealed partial record Success : Result; |
| 26 | + |
| 27 | + public sealed partial record Warning(string Message) : Result; |
| 28 | + |
| 29 | + public sealed partial record Error(int Code) : Result; |
| 30 | +} |
| 31 | +``` |
| 32 | +Alternatively, a union can be defined with unnested case classes and interfaces, allowing the possibility of creating dimensional unions (see below). |
| 33 | + |
| 34 | +### Evaluation |
| 35 | +```csharp |
| 36 | +var message = result switch |
| 37 | +{ |
| 38 | + Result.Error { Code: > 70 } error => $"High Error code: {error.Code}", |
| 39 | + Result.Error error => $"Error code: {error.Code}", |
| 40 | + Result.Warning { Message: "Tough warning" } => "Not good", |
| 41 | + Result.Warning warning => warning.Message, |
| 42 | + Result.Success => "Great", |
| 43 | +}; |
| 44 | +``` |
| 45 | + |
| 46 | +### Dimensional unions |
| 47 | +To support dimensional unions, unnested cases help because the cases are no longer defined inside a union. However, for this to work the unions are required to declare a factory method named exactly like the case type and that has the CaseType attribute specifying the actual type. |
| 48 | +Since version 3, factory methods are generated when the union is declared partial. Alternatively, a code fix (PDU0001) is available to generate the factory methods. |
| 49 | + |
| 50 | +```csharp |
| 51 | +[Sundew.DiscriminatedUnions.DiscriminatedUnion] |
| 52 | +public partial interface IExpression; |
| 53 | + |
| 54 | +[Sundew.DiscriminatedUnions.DiscriminatedUnion] |
| 55 | +public partial interface IArithmeticExpression : IExpression; |
| 56 | + |
| 57 | +[Sundew.DiscriminatedUnions.DiscriminatedUnion] |
| 58 | +public partial interface ICommutativeExpression : IArithmeticExpression; |
| 59 | + |
| 60 | +public sealed partial record AdditionExpression(IExpression Lhs, IExpression Rhs) : ICommutativeExpression; |
| 61 | + |
| 62 | +public sealed partial record SubtractionExpression(IExpression Lhs, IExpression Rhs) : IArithmeticExpression; |
| 63 | + |
| 64 | +public sealed partial record MultiplicationExpression(IExpression Lhs, IExpression Rhs) : ICommutativeExpression; |
| 65 | + |
| 66 | +public sealed partial record DivisionExpression(IExpression Lhs, IExpression Rhs) : IArithmeticExpression; |
| 67 | + |
| 68 | +public sealed partial record ValueExpression(int Value) : IExpression; |
| 69 | +``` |
| 70 | + |
| 71 | +#### Evaluating dimensional unions |
| 72 | +With dimensional unions it is possible to handle all cases using a sub union. |
| 73 | +As seen in the example below, handling the ArithmeticExpression covers Addition-, Subtraction-, Multiplication- and DivisionExpression. |
| 74 | +Typically one would dispatch these to a method handling ArithmeticExpression and where handling all cases would be checked, but it is not required. |
| 75 | +This makes it convienient to separate handling logic in smaller chucks of code. |
| 76 | + |
| 77 | +```csharp |
| 78 | +public int Evaluate(Expression expression) |
| 79 | +{ |
| 80 | + return expression switch |
| 81 | + { |
| 82 | + ArithmeticExpression arithmeticExpression => Evaluate(arithmeticExpression), |
| 83 | + ValueExpression valueExpression => valueExpression.Value, |
| 84 | + }; |
| 85 | +} |
| 86 | + |
| 87 | +public int Evaluate(ArithmeticExpression arithmeticExpression) |
| 88 | +{ |
| 89 | + return arithmeticExpression switch |
| 90 | + { |
| 91 | + AdditionExpression additionExpression => Evaluate(additionExpression.Lhs) + Evaluate(additionExpression.Rhs), |
| 92 | + SubtractionExpression subtractionExpression => Evaluate(subtractionExpression.Lhs) - Evaluate(subtractionExpression.Rhs), |
| 93 | + MultiplicationExpression multiplicationExpression => Evaluate(multiplicationExpression.Lhs) * Evaluate(multiplicationExpression.Rhs), |
| 94 | + DivisionExpression divisionExpression => Evaluate(divisionExpression.Lhs) / Evaluate(divisionExpression.Rhs), |
| 95 | + }; |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +#### Enum evaluation |
| 100 | +As of version 5.1, regular enums can also use the DiscriminatedUnion attribute causing the analyzer to exhaustively check switch statements and expressions. |
| 101 | + |
| 102 | +## Generator features |
| 103 | +As mentioned a source generator is automatically activated for generating factory methods when the partial keyword is specified. |
| 104 | +In addition, the DiscriminatedUnion attribute can specify a flags enum (GeneratorFeatures) to control additional code generation. |
| 105 | + |
| 106 | +* Segregate - Generates an extension method for IEnumerable<TUnion> that segregates all items into buckets of the different result. |
| 107 | + |
| 108 | +## Supported diagnostics: |
| 109 | +| Diagnostic Id | Description | Code Fix | |
| 110 | +| ------------- | ------------------------------------------------------------------------------------------------------------------------- | :------: | |
| 111 | +| SDU0001 | Switch does not handled all cases | yes | |
| 112 | +| SDU0002 | Switch should not handle default case | yes | |
| 113 | +| SDU0003 | Switch has unreachable null case | yes | |
| 114 | +| SDU0004 | Class unions must be abstract | yes | |
| 115 | +| SDU0005 | Only unions can extended other unions | no | |
| 116 | +| SDU0006 | Unions cannot be extended outside their assembly | no | |
| 117 | +| SDU0007 | Cases must be declared in the same assembly as their unions | no | |
| 118 | +| SDU0008 | Cases should be sealed | yes | |
| 119 | +| SDU0009 | Unnested cases should have factory method | PDU0001 | |
| 120 | +| SDU0010 | Factory method should have correct CaseTypeAttribute | yes | |
| 121 | +| SDU0011 | Reported when a case is implemented by throwing NotImplementedException, because CodeCleanup may siliently 'fix' SDU0001. | yes | |
| 122 | +| SDU0012 | Reported when a case contains type parameters that are not in the union type parameter list. | yes | |
| 123 | +| PDU0001 | Make union/case partial for code generator | yes | |
| 124 | +| PDU0002 | Populate union factory methods | yes | |
| 125 | +| SDU9999 | Switch should throw in default case | no | |
| 126 | +| GDU0001 | Discriminated union declaration could not be found | no | |
| 127 | + |
| 128 | +## Issues/Todos |
| 129 | +* Switch appears with red squiggly lines in VS: https://github.com/dotnet/roslyn/issues/57041 |
| 130 | +* Nullability is falsely evaluated when the switch hints null is possible: https://github.com/dotnet/roslyn/issues/57042 |
0 commit comments