├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── CSharp.UnionTypes.sln ├── LICENSE.txt ├── README.md ├── RELEASE_NOTES.md ├── docs ├── index.md └── tutorial.md ├── src ├── CSharp.UnionTypes.SourceGenerator │ ├── AST.fs │ ├── CSharp.UnionTypes.SourceGenerator.fsproj │ ├── CodeEmitter.fs │ ├── Parser.fs │ └── SourceGenerator.fs ├── CSharp.UnionTypes.TestApplication │ ├── CSharp.UnionTypes.TestApplication.csproj │ ├── Program.cs │ └── maybe.csunion └── Key.snk └── tests └── Tests.CSharp.UnionTypes.SourceGenerator ├── CodeEmitterTests.fs ├── ParserTests.fs ├── SourceGeneratorTests.fs └── Tests.CSharp.UnionTypes.SourceGenerator.fsproj /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp text=auto eol=lf 6 | *.vb diff=csharp text=auto eol=lf 7 | *.fs diff=csharp text=auto eol=lf 8 | *.fsi diff=csharp text=auto eol=lf 9 | *.fsx diff=csharp text=auto eol=lf 10 | *.sln text eol=crlf merge=union 11 | *.csproj merge=union 12 | *.vbproj merge=union 13 | *.fsproj merge=union 14 | *.dbproj merge=union 15 | 16 | # Standard to msysgit 17 | *.doc diff=astextplain 18 | *.DOC diff=astextplain 19 | *.docx diff=astextplain 20 | *.DOCX diff=astextplain 21 | *.dot diff=astextplain 22 | *.DOT diff=astextplain 23 | *.pdf diff=astextplain 24 | *.PDF diff=astextplain 25 | *.rtf diff=astextplain 26 | *.RTF diff=astextplain 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Please provide a succinct description of your issue. 4 | 5 | ### Repro steps 6 | 7 | Please provide the steps required to reproduce the problem 8 | 9 | 1. Step A 10 | 11 | 2. Step B 12 | 13 | ### Expected behavior 14 | 15 | Please provide a description of the behavior you expect. 16 | 17 | ### Actual behavior 18 | 19 | Please provide a description of the actual behavior you observe. 20 | 21 | ### Known workarounds 22 | 23 | Please provide a description of any known workarounds. 24 | 25 | ### Related information 26 | 27 | * Operating system 28 | * Branch 29 | * .NET Runtime, CoreCLR or Mono Version 30 | * Performance information, links to performance testing scripts 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET CI 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "*" ] 11 | release: 12 | types: 13 | - published # Run the workflow when a new GitHub release is published 14 | - edited 15 | 16 | env: 17 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 18 | DOTNET_NOLOGO: true 19 | NuGetDirectory: ${{ github.workspace}}/nuget 20 | 21 | defaults: 22 | run: 23 | shell: pwsh 24 | 25 | jobs: 26 | build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 # Get all history to allow automatic versioning using MinVer 32 | 33 | - name: Setup .NET 34 | uses: actions/setup-dotnet@v4 35 | with: 36 | dotnet-version: 8.0.x 37 | 38 | - name: Build 39 | run: dotnet build --configuration Release --property:PackageOutputPath=${{ env.NuGetDirectory }} 40 | 41 | - name: Test 42 | run: dotnet test --configuration Release --no-build --verbosity normal 43 | 44 | #- name: Pack 45 | # run: dotnet pack --configuration Release "src/CSharp.UnionTypes.SourceGenerator" --output ${{ env.NuGetDirectory }} 46 | 47 | # Publish the NuGet package as an artifact, so they can be used in the following jobs 48 | - uses: actions/upload-artifact@v4 49 | with: 50 | name: nuget 51 | if-no-files-found: error 52 | retention-days: 7 53 | path: ${{ env.NuGetDirectory }}/*.nupkg 54 | 55 | # validate_nuget: 56 | # runs-on: ubuntu-latest 57 | # needs: [ create_nuget ] 58 | # steps: 59 | # # Install the .NET SDK indicated in the global.json file 60 | # - name: Setup .NET 61 | # uses: actions/setup-dotnet@v4 62 | 63 | # # Download the NuGet package created in the previous job 64 | # - uses: actions/download-artifact@v3 65 | # with: 66 | # name: nuget 67 | # path: ${{ env.NuGetDirectory }} 68 | 69 | # - name: Install nuget validator 70 | # run: dotnet tool update Meziantou.Framework.NuGetPackageValidation.Tool --global 71 | 72 | # # Validate metadata and content of the NuGet package 73 | # # https://www.nuget.org/packages/Meziantou.Framework.NuGetPackageValidation.Tool#readme-body-tab 74 | # # If some rules are not applicable, you can disable them 75 | # # using the --excluded-rules or --excluded-rule-ids option 76 | # - name: Validate package 77 | # run: meziantou.validate-nuget-package (Get-ChildItem "${{ env.NuGetDirectory }}/*.nupkg") 78 | 79 | publish: 80 | # Publish only when creating a GitHub Release 81 | # https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository 82 | # You can update this logic if you want to manage releases differently 83 | if: github.event_name == 'release' 84 | runs-on: ubuntu-latest 85 | needs: 86 | - build 87 | steps: 88 | # Download the NuGet package created in the previous job 89 | - uses: actions/download-artifact@v4 90 | with: 91 | name: nuget 92 | path: ${{ env.NuGetDirectory }} 93 | 94 | # Install the .NET SDK indicated in the global.json file 95 | - name: Setup .NET Core 96 | uses: actions/setup-dotnet@v4 97 | 98 | # Publish all NuGet packages to NuGet.org 99 | # Use --skip-duplicate to prevent errors if a package with the same version already exists. 100 | # If you retry a failed workflow, already published packages will be skipped without error. 101 | - name: Publish NuGet package 102 | run: | 103 | foreach($file in (Get-ChildItem "${{ env.NuGetDirectory }}" -Recurse -Include *.nupkg)) { 104 | dotnet nuget push $file --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate 105 | } 106 | 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Xamarin Studio / monodevelop user-specific 10 | *.userprefs 11 | *.dll.mdb 12 | *.exe.mdb 13 | 14 | # Build results 15 | 16 | [Dd]ebug/ 17 | [Rr]elease/ 18 | x64/ 19 | build/ 20 | [Bb]in/ 21 | [Oo]bj/ 22 | 23 | # MSTest test Results 24 | [Tt]est[Rr]esult*/ 25 | [Bb]uild[Ll]og.* 26 | 27 | *_i.c 28 | *_p.c 29 | *.ilk 30 | *.meta 31 | *.obj 32 | *.pch 33 | *.pdb 34 | *.pgc 35 | *.pgd 36 | *.rsp 37 | *.sbr 38 | *.tlb 39 | *.tli 40 | *.tlh 41 | *.tmp 42 | *.tmp_proj 43 | *.log 44 | *.vspscc 45 | *.vssscc 46 | .builds 47 | *.pidb 48 | *.log 49 | *.scc 50 | 51 | # Visual C++ cache files 52 | ipch/ 53 | *.aps 54 | *.ncb 55 | *.opensdf 56 | *.sdf 57 | *.cachefile 58 | 59 | # Visual Studio profiler 60 | *.psess 61 | *.vsp 62 | *.vspx 63 | 64 | # Other Visual Studio data 65 | .vs/ 66 | 67 | # Guidance Automation Toolkit 68 | *.gpState 69 | 70 | # ReSharper is a .NET coding add-in 71 | _ReSharper*/ 72 | *.[Rr]e[Ss]harper 73 | 74 | # TeamCity is a build add-in 75 | _TeamCity* 76 | 77 | # DotCover is a Code Coverage Tool 78 | *.dotCover 79 | 80 | # NCrunch 81 | *.ncrunch* 82 | .*crunch*.local.xml 83 | 84 | # Installshield output folder 85 | [Ee]xpress/ 86 | 87 | # DocProject is a documentation generator add-in 88 | DocProject/buildhelp/ 89 | DocProject/Help/*.HxT 90 | DocProject/Help/*.HxC 91 | DocProject/Help/*.hhc 92 | DocProject/Help/*.hhk 93 | DocProject/Help/*.hhp 94 | DocProject/Help/Html2 95 | DocProject/Help/html 96 | 97 | # Click-Once directory 98 | publish/ 99 | 100 | # Publish Web Output 101 | *.Publish.xml 102 | 103 | # Enable nuget.exe in the .nuget folder (though normally executables are not tracked) 104 | !.nuget/NuGet.exe 105 | 106 | # Windows Azure Build Output 107 | csx 108 | *.build.csdef 109 | 110 | # Windows Store app package directory 111 | AppPackages/ 112 | 113 | # VSCode 114 | .vscode/ 115 | 116 | # Others 117 | sql/ 118 | *.Cache 119 | ClientBin/ 120 | [Ss]tyle[Cc]op.* 121 | ~$* 122 | *~ 123 | *.dbmdl 124 | *.[Pp]ublish.xml 125 | *.pfx 126 | *.publishsettings 127 | 128 | # RIA/Silverlight projects 129 | Generated_Code/ 130 | 131 | # Backup & report files from converting an old project file to a newer 132 | # Visual Studio version. Backup files are not needed, because we have git ;-) 133 | _UpgradeReport_Files/ 134 | Backup*/ 135 | UpgradeLog*.XML 136 | UpgradeLog*.htm 137 | 138 | # SQL Server files 139 | App_Data/*.mdf 140 | App_Data/*.ldf 141 | 142 | 143 | #LightSwitch generated files 144 | GeneratedArtifacts/ 145 | _Pvt_Extensions/ 146 | ModelManifest.xml 147 | 148 | # ========================= 149 | # Windows detritus 150 | # ========================= 151 | 152 | # Windows image file caches 153 | Thumbs.db 154 | ehthumbs.db 155 | 156 | # Folder config file 157 | Desktop.ini 158 | 159 | # Recycle Bin used on file shares 160 | $RECYCLE.BIN/ 161 | 162 | # Mac desktop service store files 163 | .DS_Store 164 | 165 | # =================================================== 166 | # Exclude F# project specific directories and files 167 | # =================================================== 168 | 169 | # NuGet Packages Directory 170 | packages/ 171 | 172 | # Generated documentation folder 173 | docs/output/ 174 | 175 | # Temp folder used for publishing docs 176 | temp/ 177 | 178 | # Test results produced by build 179 | TestResults.xml 180 | 181 | # Nuget outputs 182 | nuget/*.nupkg 183 | release.cmd 184 | release.sh 185 | localpackages/ 186 | paket-files 187 | *.orig 188 | .paket/paket.exe 189 | docs/content/license.md 190 | docs/content/release-notes.md 191 | .fake 192 | docs/tools/FSharp.Formatting.svclog 193 | *.g.cs 194 | -------------------------------------------------------------------------------- /CSharp.UnionTypes.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.11.34826.228 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "CSharp.UnionTypes.SourceGenerator", "src\CSharp.UnionTypes.SourceGenerator\CSharp.UnionTypes.SourceGenerator.fsproj", "{D7806561-B646-4523-BE38-48691B4AF683}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharp.UnionTypes.TestApplication", "src\CSharp.UnionTypes.TestApplication\CSharp.UnionTypes.TestApplication.csproj", "{7E8FE3DA-41F6-4E2C-A9C2-26C1262E606E}" 8 | EndProject 9 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Tests.CSharp.UnionTypes.SourceGenerator", "tests\Tests.CSharp.UnionTypes.SourceGenerator\Tests.CSharp.UnionTypes.SourceGenerator.fsproj", "{B39E883D-B0AE-493F-BD2D-C1699D041815}" 10 | EndProject 11 | Global 12 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 13 | Debug|Any CPU = Debug|Any CPU 14 | Release|Any CPU = Release|Any CPU 15 | EndGlobalSection 16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 17 | {D7806561-B646-4523-BE38-48691B4AF683}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {D7806561-B646-4523-BE38-48691B4AF683}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {D7806561-B646-4523-BE38-48691B4AF683}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {D7806561-B646-4523-BE38-48691B4AF683}.Release|Any CPU.Build.0 = Release|Any CPU 21 | {7E8FE3DA-41F6-4E2C-A9C2-26C1262E606E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {7E8FE3DA-41F6-4E2C-A9C2-26C1262E606E}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {7E8FE3DA-41F6-4E2C-A9C2-26C1262E606E}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {7E8FE3DA-41F6-4E2C-A9C2-26C1262E606E}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {B39E883D-B0AE-493F-BD2D-C1699D041815}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {B39E883D-B0AE-493F-BD2D-C1699D041815}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {B39E883D-B0AE-493F-BD2D-C1699D041815}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {B39E883D-B0AE-493F-BD2D-C1699D041815}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {89833BB8-2427-4FB8-8FA3-E35F7C9BCCCF} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2016 John Azariah 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # csharp-uniontypes 2 | 3 | Languages like F#, Scala and Haskell have special types to represent a choice of a finite set of values. These types are variously called 'Union Types', 'Sum Types' or 'Discriminated Unions (DUs)'. 4 | 5 | Union Types are a powerful way of representing choices. They enforce value semantics and can represent choices between other Record and Union types. They are very useful constructs because they can help model the domain of a problem more precisely, and can help eliminate entire classes of runtime bugs. 6 | 7 | Modern C# provides record types, which implicitly implement value semantics; and has support for pattern matching - both of which make implementation of Union Types possible, if tedious. 8 | 9 | This library relieves us of the tedium of building out boilerplate code for Union Types. Instead, one is able to define Union Types in a DSL with syntax that is familiar to C# users, and have the source-generator based library generate the necessary code to support pattern matching and other idiomatic C# features. 10 | 11 | The objects generated are extensible so additional methods can be added to them allowing these Union Types to be used in a rich domain model. 12 | 13 | ## Build Status 14 | 15 | ## Maintainer(s) 16 | 17 | - [@johnazariah](https://github.com/johnazariah) 18 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | #### 1.0.1 - Jul 15 2024 2 | * Renamed package to JohnAz.CSharp.UnionTypes 3 | * Use modern (C# 9+) idioms with `record` types to provide value semantics and pattern matching 4 | * Use GitVersion for semantic versioning 5 | 6 | #### 1.0.1 - Jan 10 2017 7 | * SingleFileGenerator and VSIX also published 8 | * Support for constrained types 9 | 10 | #### 1.0.0 - Dec 31 2016 11 | * Initial release of C# Discriminated Union types 12 | * Parser and Roslyn-Based Code Generator library for Discriminated Unions 13 | * Command Line Executable 14 | 15 | #### 0.0.1-beta - Dec 13 2016 16 | * Changed name from fsharp-project-scaffold to CSharp.UnionTypes 17 | * Initial release 18 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | CSharp.UnionTypes 2 | ======================== 3 | 4 | ## Documentation 5 | 6 | ### Install 7 | ------- 8 | 9 | The JohnAz.CSharp.UnionTypes library can be [installed from NuGet](https://www.nuget.org/packages/JohnAz.CSharp.UnionTypes). 10 | 11 |
PM> NuGet\Install-Package JohnAz.CSharp.UnionTypes
12 | 13 | ### Use 14 | ------- 15 | 16 | * Define a union type in a `.csunion` file. We have a special DSL for this with a syntax that should be familiar to C# users: 17 | 18 | ```csharp 19 | namespace Monads 20 | { 21 | union Maybe { Some | None }; 22 | } 23 | ``` 24 | 25 | This indicates that a `Maybe` type is _either_ a `Some` wrapping a value of type `T`, or a `None` "marker" value. 26 | 27 | * This `.csunion` file is processed by a [Source Generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview), which then generates the appropriate objects to implement the discriminated union in C#. 28 | 29 | Using value semantics provided by records in C# 9.0 and above, we can then generate the following code which implements the discriminated union. 30 | 31 | ```csharp 32 | namespace Monads 33 | { 34 | public abstract partial record Maybe 35 | { 36 | private Maybe() { } 37 | public sealed partial record Some(T Value) : Maybe; 38 | public sealed partial record None() : Maybe; 39 | } 40 | } 41 | ``` 42 | 43 | * We can then use the `Maybe` type in our code with of [switch expressions](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/switch-expression) to properly handle the various cases. 44 | 45 | ```csharp 46 | public static void Main (string[] args) 47 | { 48 | Maybe m23 = new Maybe.Some(23); 49 | 50 | Console.WriteLine(m23 switch 51 | { 52 | Maybe.Some { Value: var v } => $"Some {v}", 53 | Maybe.None => "None", 54 | _ => throw new NotImplementedException() 55 | }); 56 | } 57 | ``` 58 | 59 | In the code above, we create a `Maybe` value of type `Maybe.Some` wrapping an `int` value of `23`, and then use a switch expression to print out the value `Some 23`. 60 | 61 | To summarize 62 | 63 | 1. Install the [Nuget Package](https://www.nuget.org/packages/JohnAz.CSharp.UnionTypes) 64 | 2. Add a file of type `.csunion` in your project and set its type to `Analyser Additional File`. 65 | 3. Write an expression to define a Union type in the `.csunion` file above. 66 | 4. Use the type as if you had written it yourself in C#. The source generator will have generated it for you in the background. 67 | 5. Enjoy! 68 | 69 | ### Contributing and copyright 70 | -------------------------- 71 | 72 | The project is hosted on [GitHub][gh] where you can [report issues][issues], fork 73 | the project and submit pull requests. If you're adding a new public API, please also 74 | consider adding [samples][content] that can be turned into a documentation, or consider improving the [tutorial](tutorial.md). You might 75 | also want to read the [library design notes][readme] to understand how it works. 76 | 77 | The library is available under Public Domain license, which allows modification and 78 | redistribution for both commercial and non-commercial purposes. For more information see the 79 | [License file][license] in the GitHub repository. 80 | 81 | [content]: https://github.com/johnazariah/csharp-uniontypes/tree/master/docs/content 82 | [gh]: https://github.com/johnazariah/csharp-uniontypes 83 | [issues]: https://github.com/johnazariah/csharp-uniontypes/issues 84 | [readme]: https://github.com/johnazariah/csharp-uniontypes/blob/master/README.md 85 | [license]: https://github.com/johnazariah/csharp-uniontypes/blob/master/LICENSE.txt 86 | -------------------------------------------------------------------------------- /docs/tutorial.md: -------------------------------------------------------------------------------- 1 | 2 | Union Types for C# 3 | ======================== 4 | 5 | ## Summary 6 | 7 | This library provides a source-generator based solution to allow modelling of union types within a C# project. 8 | 9 | It defines a minimal extension to the C# language, and automatically generate idiomatic C# classes which provide the functionality of union types. 10 | 11 | ## Structure of a .csunion file 12 | 13 | The `.csunion` file should have the following structure: 14 | 15 | ``` 16 | namespace 17 | { 18 | using ; 19 | 20 | union 21 | { 22 | | | ; 23 | } 24 | } 25 | ``` 26 | 27 | ### namespace 28 | 29 | The outermost element must be a _single_ `namespace` element with a valid (possibly fully-qualified) namespace-name. 30 | 31 | The classes generated will be placed into this namespace, and your C# code can simply reference the namespace with a normal `using`.abs 32 | 33 | ### using 34 | 35 | Any number of `using` statements can be present. Each should terminate with a `;` and be on a separate line. 36 | 37 | The generated file will include these `using` statements. 38 | 39 | Normally valid `using` statements are supported, but aliasing is not. 40 | 41 | This element is supported so that the generated file can compile without further editing, so typically you will need to specify assemblies containing types referenced by the union type.abs 42 | 43 | _The `System` and `System.Collections` namespaces are always included automatically even without explicit specification._ 44 | 45 | ### union 46 | 47 | Any number of `union` statements can be present. Each may terminate with a `;` and must start on a separate line. 48 | 49 | This element specifies the Discriminated Union type. 50 | 51 | The structure of the `union` statement is: 52 | 53 | ``` 54 | union 55 | { 56 | | | ; 57 | } 58 | ``` 59 | 60 | * union-name : This will be the name of the Union Type generated. It should be a valid class name, and can specify type arguments if the union type is to be generic. 61 | _You cannot specify generic type constraints in this file. Create a partial class with the same name and type arguments in another `.cs` file in the project and include the generic type constraints on that._ 62 | 63 | * union-member : There can be any number of members. Each member represents a choice in the union. 64 | A union members can either be: 65 | * Singleton : In this case, the union-member has exactly one instance with the same name as the member. 66 | * Value-Constructor : In this case, the union-member is parametrized by the type specified, and will be an instance of a class with the same name as the member associated with a value of the parametrizing type. 67 | 68 | Example: 69 | ``` 70 | union Maybe { None | Some } 71 | ``` 72 | This specifies that `Maybe` is either the value `None`, or `Some` with an associated `int`. 73 | 74 | Some illustrative examples are: 75 | 76 | #### Enumerations 77 | 78 | ``` 79 | union TrafficLights { Red | Amber | Green } 80 | ``` 81 | 82 | This discriminated union type is a union of singleton values. _i.e._ The `TrafficLights` type can have a value that is either `Red`, `Amber` or `Green`. These _enum_-like types are very useful in providing closed sets of values, and also with _constrained types_. 83 | 84 | #### Value Constructors 85 | 86 | ``` 87 | union Maybe { None | Some } 88 | ``` 89 | This discriminated union type represents a choice between the singleton value `None`, and an instance of a class `Some` wrapping a value of type `T`. In this case, the discriminated union type is itself generic and takes `T` as a type argument. 90 | 91 | _Note that this discriminated union shows the use of a choice between a singleton and a parametrized value. Such choices are perfectly legal_ 92 | 93 | ``` 94 | union Payment { Cash | CreditCard | Cheque } 95 | ``` 96 | This discriminated union type is non generic and represents a choice between an instance of `Cash` (parameterized by `Amount`), `CreditCard` (parametrized by `CreditCardDetails`), and `Cheque` (parametrized by `ChequeDetails`). 97 | 98 | _Note that in this case, one or more `using` directives including the assembly (or assemblies) containing the definitions of `Amount`, `CreditCardDetails`, and `ChequeDetails` will need to be specified for the generated file to compile._ 99 | 100 | ``` 101 | union Either { Left | Right } 102 | ``` 103 | This discriminated union demonstrates multiple type parameters. 104 | 105 | #### Constrained Types 106 | ``` 107 | union TrafficLightsToStopFor constrains TrafficLights { Red | Amber } 108 | ``` 109 | Typically, classes are specified with base functionality, which can be augmented by derived classes. With union types, however, there is often a benefit to defining a type that represents _a subset of_ another type's members. 110 | 111 | The `constrains` keyword allows for such a specification. 112 | 113 | * **It is illegal to specify a member in a constrained type that does not exist in the type it is constraining.** 114 | 115 | ## How to code against a union type 116 | 117 | Once the specification has been transformed into a C# class, it can be directly used in any C# code. 118 | 119 | ### Specifying the Choice 120 | 121 | Creating an instance of the union type does not involve `new`. Indeed, it is not possible to `new` up a Union Type because it is represented by an abstract class. 122 | 123 | Instead, one must use static members provided in the abstract class to construct instances as desired. 124 | 125 | #### 'Singleton' choices 126 | For singleton choices, you can simply reference the readonly singleton member as follows: 127 | 128 | ``` 129 | var none = new Maybe.None(); 130 | ``` 131 | 132 | #### 'Value Constructor' choices 133 | For value constructor choices, you will need to provide the value to the constructor member as follows: 134 | 135 | ``` 136 | var name = new Maybe.Some("John"); 137 | ``` 138 | 139 | ### Augmenting the partial class 140 | 141 | All the generated code is in the form of partial classes, which allows methods to be attached to the Union Type from within the C# project. 142 | 143 | For example, we can extend the `Maybe` class with functional typeclasses by providing an associate C# file defining another partial class of it. 144 | 145 | ``` 146 | public partial class Maybe 147 | { 148 | // Functor 149 | public Maybe Map(Func f) => Match(() => Maybe.None, _ => Maybe.Some(f(_))); 150 | 151 | // Applicative 152 | public static Maybe Apply(Maybe> f, Maybe x) => f.Match(() => Maybe.None, _f => x.Match(() => Maybe.None, _x => Maybe.Some(_f(_x)))); 153 | 154 | // Foldable 155 | public R Fold(R z, Func f) => Match(() => z, _ => f(z, _)); 156 | 157 | // Monad 158 | public static Maybe Unit(T value) => Some(value); 159 | public Maybe Bind(Func> f) => Match(() => Maybe.None, f); 160 | 161 | public T GetOrElse(T defaultValue) => Fold(defaultValue, (_, v) => v); 162 | } 163 | 164 | // LINQ extensions 165 | public static class MaybeLinqExtensions 166 | { 167 | public static Maybe Lift(this T value) => Maybe.Unit(value); 168 | 169 | public static Maybe Select(this Maybe m, Func f) => m.Map(f); 170 | 171 | public static Maybe SelectMany(this Maybe m, Func> f) => m.Bind(f); 172 | 173 | public static Maybe SelectMany(this Maybe m, Func> f, Func s) 174 | => m.SelectMany(x => f(x).SelectMany(y => (Maybe.Unit(s(x, y))))); 175 | } 176 | ``` 177 | 178 | which allows us to use the class in fully augmented form in our code as follows: 179 | 180 | ``` 181 | var lenOpt = from s in Maybe.Some("Hello, World") 182 | select s.Length; 183 | Console.WriteLine($"The length of the given string is {lenOpt.GetOrElse(0)}"); 184 | ``` 185 | 186 | ### Value Semantics 187 | 188 | The union type implementation uses classes, which means that simply comparing two instances for equality within C# code will result in reference comparision. 189 | 190 | However, we want to make sure that `Maybe.Some("A")` will always be equal to `Maybe.Some("A")` - regardless of whether they were instantiated separately or are both the same object. 191 | 192 | The generated code for union types implement `IEquatable` and `IStructuralEquatable` for each union, and override the appropriate types to provide value semantics for equality. 193 | 194 | ``` 195 | [Test] 196 | public void Some_equals_Some() 197 | { 198 | Assert.True(Maybe.Some(10).Equals(Maybe.Some(10))); 199 | Assert.True(Maybe.Some(10) == Maybe.Some(10)); 200 | Assert.False(Maybe.Some(10) != Maybe.Some(10)); 201 | } 202 | ``` 203 | 204 | ## Background : Algebraic Data Types 205 | [Algebraic Data Types](https://en.wikipedia.org/wiki/Algebraic_data_type) are composite data types - types that are made up of other types. 206 | 207 | ### Product Types 208 | In C#, we have only one way of combining types - creating "named tuples" (structs and classes with named properties) and "anonymous tuples" (instances of `Tuple<>`). 209 | 210 | In type algebraic terms, these are called _Product Types_ because the number of valid values of the composite type is the product of the number of valid values of the property types). 211 | For example, the following struct has 512 possible values because the constituent components have 256 and 2 possible values respectively 212 | 213 | ``` 214 | struct F 215 | { 216 | char CharacterValue { get; } // 256 possible values 217 | bool BooleanFlag { get; } // 2 possible values 218 | ... 219 | } // 256 * 2 = 512 possible values 220 | ``` 221 | 222 | Such types are commonly used as encapsulation mechanisms, and for keeping related items together. 223 | 224 | ### Sum Types 225 | However, languages like F# and Scala have another way of combining types which proves to be very useful in Domain Design. They can be used to specify states very precisely and help make [illegal states unrepresentable](http://www.slideshare.net/ScottWlaschin/domain-driven-design-with-the-f-type-system-functional-londoners-2014). 226 | 227 | These types are variously known as _Choice Types_, _Discriminated Union Types_ or _Sum Types_. The valid values of a such a composite type is the _sum_ of the constituent types. 228 | 229 | C# programmers can think of these types as "Enums on Steroids", because they represent a choice between values of disparate types. 230 | 231 | Consider a domain requirement that stipulates that a payment is recorded as exactly one of the following: 232 | 233 | * Cash, where we capture a fixed precision number and an associated currency 234 | * Cheque, where we capture information pertinent to the cheque 235 | * Credit Card, where we capture information pertinent to the credit card 236 | 237 | In other words, we require to model a _choice_ between disparate things. Without a formal composite type to model choices, we are generally left to rolling our own mechanisms involving enums and structs. 238 | 239 | However, in F#, we may succintly represent a valid payment as follows: 240 | 241 | ``` 242 | type Payment = 243 | | Cash of Amount // the currency and amount paid 244 | | Cheque of ChequeDetails // the amount, bank id, cheque number, date ... 245 | | CreditCard of CardDetails // the amount, credit-card type, credit-card expiry date ... 246 | ``` 247 | 248 | This is far more precise than a record which may introduce illegal states where more than one payment method could be set. 249 | 250 | Indeed, if one was willing to include a F# project in their solution and express the domain model in F#, they could simply use the F# types in C# without any further work. 251 | 252 | Alternately, one could use this library to model union-types without switching languages. 253 | -------------------------------------------------------------------------------- /src/CSharp.UnionTypes.SourceGenerator/AST.fs: -------------------------------------------------------------------------------- 1 | namespace CSharp.UnionTypes 2 | 3 | [] 4 | module AST = 5 | let toDottedName (head, (dotComponents : string list)) = 6 | let tail' = dotComponents |> String.concat "." 7 | 8 | let tail = 9 | if (tail' <> "") then ("." + tail') 10 | else "" 11 | sprintf "%s%s" head tail 12 | 13 | type Namespace = 14 | { NamespaceName : NamespaceName 15 | NamespaceMembers : NamespaceMember list } 16 | 17 | static member apply (name, members) = 18 | { NamespaceName = name 19 | NamespaceMembers = members } 20 | 21 | member this.Usings = 22 | let isUsing = 23 | function 24 | | Using x -> Some x 25 | | _ -> None 26 | this.NamespaceMembers |> List.choose isUsing 27 | 28 | member this.Unions = 29 | let isUnion = 30 | function 31 | | UnionType x -> Some x 32 | | _ -> None 33 | this.NamespaceMembers |> List.choose isUnion 34 | 35 | override this.ToString() = 36 | let members = 37 | seq { 38 | yield! this.Usings |> List.map (fun u -> u.unapply) 39 | yield! this.Unions |> List.map (fun u -> u.UnionClassNameWithTypeArgs) 40 | } 41 | |> String.concat ("; ") 42 | sprintf @"namespace %s{%s}" this.NamespaceName.unapply members 43 | 44 | and NamespaceName = 45 | | NamespaceName of string 46 | static member apply = toDottedName >> NamespaceName 47 | 48 | member this.unapply = 49 | match this with 50 | | NamespaceName x -> x 51 | 52 | override this.ToString() = this.unapply 53 | 54 | and NamespaceMember = 55 | | Using of UsingName 56 | | UnionType of UnionType 57 | override this.ToString() = 58 | match this with 59 | | Using x -> x.ToString() 60 | | UnionType x -> x.ToString() 61 | 62 | and UsingName = 63 | | UsingName of string 64 | static member apply = toDottedName >> UsingName 65 | 66 | member this.unapply = 67 | match this with 68 | | UsingName x -> x 69 | 70 | override this.ToString() = this.unapply 71 | 72 | and UnionType = 73 | { UnionTypeName : UnionTypeName 74 | UnionTypeParameters : TypeParameter list 75 | UnionMembers : UnionMember list 76 | BaseType : FullTypeName option } 77 | 78 | static member apply (((unionTypeName, typeArgumentListOption), baseType), unionMemberList) = 79 | { UnionTypeName = unionTypeName 80 | UnionMembers = unionMemberList 81 | UnionTypeParameters = typeArgumentListOption |> Option.fold (fun _ s -> s) [] 82 | BaseType = baseType } 83 | 84 | member this.UnionClassName = 85 | this.UnionTypeName.unapply 86 | 87 | member this.UnionClassNameWithTypeArgs = 88 | let typeParameters = 89 | this.UnionTypeParameters 90 | |> Seq.map (fun a -> $"{a}") 91 | |> String.concat ", " 92 | |> (fun a -> 93 | if a <> "" 94 | then sprintf "<%s>" a 95 | else "") 96 | 97 | let bareTypeName = this.UnionTypeName.unapply 98 | in 99 | sprintf "%s%s" bareTypeName typeParameters 100 | 101 | member this.UnionClassNameWithTypeofTypeArgs = 102 | let typeParameters = 103 | this.UnionTypeParameters 104 | |> Seq.map (fun a -> $"{{typeof({a})}}") 105 | |> String.concat ", " 106 | |> (fun a -> 107 | if a <> "" 108 | then sprintf "<%s>" a 109 | else "") 110 | 111 | let bareTypeName = this.UnionTypeName.unapply 112 | in 113 | sprintf "%s%s" bareTypeName typeParameters 114 | 115 | override this.ToString() = 116 | let members = 117 | this.UnionMembers 118 | |> Seq.map (fun m -> m.ToString()) 119 | |> String.concat " | " 120 | sprintf "union %s ::= [ %s ]" this.UnionClassNameWithTypeArgs members 121 | 122 | and UnionTypeName = 123 | | UnionTypeName of string 124 | 125 | member this.unapply = 126 | match this with 127 | | UnionTypeName x -> x 128 | 129 | override this.ToString() = this.unapply 130 | 131 | and TypeParameter = 132 | | TypeArgument of string 133 | 134 | member this.unapply = 135 | match this with 136 | | TypeArgument x -> x 137 | 138 | override this.ToString() = this.unapply 139 | 140 | and UnionMember = 141 | { MemberName : UnionMemberName 142 | MemberArgumentType : FullTypeName option } 143 | 144 | static member apply(memberName, typeArgument) = 145 | { MemberName = memberName 146 | MemberArgumentType = typeArgument } 147 | 148 | member this.UnionMemberClassNameWithTypeArgs = 149 | let typeParameters = 150 | match this.MemberArgumentType with 151 | | Some mat -> sprintf "<%s>" mat.CSharpTypeName 152 | | None -> "" 153 | 154 | let bareTypeName = this.MemberName.unapply 155 | in 156 | sprintf "%s%s" bareTypeName typeParameters 157 | 158 | member this.ValueMember = 159 | match this.MemberArgumentType with 160 | | Some _ -> "Value" 161 | | None -> "" 162 | 163 | member this.UnionMemberValueMember = 164 | match this.MemberArgumentType with 165 | | Some mat -> sprintf "(%s %s)" mat.CSharpTypeName this.ValueMember 166 | | None -> "()" 167 | 168 | member this.UnionMemberValueAccessor(varName) = 169 | match this.MemberArgumentType with 170 | | Some _ -> sprintf "%s.%s" varName this.ValueMember 171 | | None -> "" 172 | 173 | override this.ToString() = 174 | this.MemberArgumentType 175 | |> Option.fold (fun _ s -> sprintf "%s of %s" this.MemberName.unapply (s.ToString())) 176 | (sprintf "%s" this.MemberName.unapply) 177 | 178 | and UnionMemberName = 179 | | UnionMemberName of string 180 | 181 | member this.unapply = 182 | match this with 183 | | UnionMemberName x -> x 184 | 185 | override this.ToString() = this.unapply 186 | 187 | and FullTypeName = 188 | { FullyQualifiedTypeName : string 189 | TypeArguments : FullTypeName list } 190 | 191 | member this.CSharpTypeName = 192 | let typeArguments' = 193 | this.TypeArguments 194 | |> Seq.map (fun ta -> ta.ToString ()) 195 | |> String.concat ", " 196 | 197 | let typeArguments = 198 | if (typeArguments' <> "") then sprintf "<%s>" typeArguments' 199 | else "" 200 | 201 | sprintf "%s%s" this.FullyQualifiedTypeName typeArguments 202 | 203 | static member apply (nonGenericNamePart, typeArguments) = 204 | { FullyQualifiedTypeName = toDottedName nonGenericNamePart 205 | TypeArguments = typeArguments |> Option.fold (fun _ s -> s) [] } 206 | 207 | override this.ToString() = this.CSharpTypeName -------------------------------------------------------------------------------- /src/CSharp.UnionTypes.SourceGenerator/CSharp.UnionTypes.SourceGenerator.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | true 6 | true 7 | 8 | 9 | 10 | true 11 | 12 | false 13 | John Azariah 14 | JohnAz.CSharp.UnionTypes 15 | Discriminated Union DSL for C# 16 | Copyright (c) 2016 John Azariah 17 | https://github.com/johnazariah/csharp-uniontypes 18 | Languages like F#, Scala and Haskell have special types to represent a choice of a finite set of values. These types are variously called 'Union Types', 'Sum Types' or 'Discriminated Unions (DUs)'. 19 | 20 | Union Types are a powerful way of representing choices. They enforce value semantics and can represent choices between other Record and Union types. They are very useful constructs because they can help model the domain of a problem more precisely, and can help eliminate entire classes of runtime bugs. 21 | 22 | Modern C# provides record types, which implicitly implement value semantics; and has suport for pattern matching - both of which make implementation of Union Types possible, if tedious. 23 | 24 | This library relieves us of the tedium of building out boilerplate code for Union Types. Instead, one is able to define Union Types in a DSL with syntax that is familiar to C# users, and have the source-generator based library generate the necessary code to support pattern matching and other idiomatic C# features. 25 | 26 | The objects generated are extensible so additional methods can be added to them allowing these Union Types to be used in a rich domain model. 27 | 28 | README.md 29 | C#; CSharp; Types; Discriminated Unions; Union Types; Choice Types; Sum Types; Type Theory; DSL; 30 | LICENSE.txt 31 | 32 | 33 | 34 | 35 | 36 | True 37 | \ 38 | 39 | 40 | True 41 | \ 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | all 70 | runtime; build; native; contentfiles; analyzers; buildtransitive 71 | 72 | 73 | all 74 | runtime; build; native; contentfiles; analyzers; buildtransitive 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/CSharp.UnionTypes.SourceGenerator/CodeEmitter.fs: -------------------------------------------------------------------------------- 1 | namespace CSharp.UnionTypes 2 | 3 | [] 4 | module CodeEmitter = 5 | open System.CodeDom.Compiler 6 | 7 | let generateCodeForNamespace (ns : Namespace) : string = 8 | let indentWriter : IndentedTextWriter = new IndentedTextWriter(new System.IO.StringWriter ()) 9 | 10 | let indentAndWriteLine (str : string) = 11 | indentWriter.Indent <- indentWriter.Indent + 1 12 | indentWriter.WriteLine str 13 | indentWriter.Indent <- indentWriter.Indent - 1 14 | 15 | let generateUsing (using : UsingName) = 16 | indentAndWriteLine $"using {using};" 17 | 18 | let generateUnion (union : UnionType) = 19 | let generateUnionMember (unionMember : UnionMember) = 20 | indentAndWriteLine $"public sealed partial record {unionMember.MemberName.unapply}{unionMember.UnionMemberValueMember} : {union.UnionClassNameWithTypeArgs}" 21 | indentAndWriteLine $"{{" 22 | indentWriter.Indent <- indentWriter.Indent + 1 23 | let memberValuePattern = 24 | match unionMember.MemberArgumentType with 25 | | Some _ -> $" {{Value}}" 26 | | None -> "" 27 | indentAndWriteLine $"override public string ToString() => $\"{union.UnionClassNameWithTypeofTypeArgs}.{unionMember.MemberName.unapply}{memberValuePattern}\";" 28 | match union.BaseType with 29 | | Some baseType -> 30 | let valueAccessor = unionMember.UnionMemberValueAccessor("value") 31 | indentAndWriteLine $"public static implicit operator {unionMember.MemberName.unapply}({baseType.CSharpTypeName}.{unionMember.MemberName.unapply} value) => new {unionMember.MemberName.unapply}({valueAccessor});" 32 | indentAndWriteLine $"public static implicit operator {baseType.CSharpTypeName}.{unionMember.MemberName.unapply}({unionMember.MemberName.unapply} value) => new {baseType.CSharpTypeName}.{unionMember.MemberName.unapply}({valueAccessor});" 33 | | None -> 34 | () 35 | 36 | indentWriter.Indent <- indentWriter.Indent - 1 37 | indentAndWriteLine $"}}" 38 | 39 | 40 | indentAndWriteLine $"public abstract partial record {union.UnionClassNameWithTypeArgs}" 41 | indentAndWriteLine $"{{" 42 | 43 | indentWriter.Indent <- indentWriter.Indent + 1 44 | 45 | indentAndWriteLine $"private {union.UnionClassName}() {{ }}" 46 | 47 | union.UnionMembers 48 | |> Seq.iter generateUnionMember 49 | 50 | indentWriter.Indent <- indentWriter.Indent - 1 51 | 52 | indentAndWriteLine $"}}" 53 | 54 | indentWriter.WriteLine $"namespace {ns.NamespaceName}" 55 | indentWriter.WriteLine $"{{" 56 | 57 | ns.Usings |> Seq.iter generateUsing 58 | ns.Unions |> Seq.iter generateUnion 59 | 60 | indentWriter.WriteLine $"}}" 61 | 62 | indentWriter.InnerWriter.ToString(); 63 | 64 | let GenerateNamespaceCode (text: string) = 65 | parseTextToNamespace text 66 | |> generateCodeForNamespace 67 | 68 | -------------------------------------------------------------------------------- /src/CSharp.UnionTypes.SourceGenerator/Parser.fs: -------------------------------------------------------------------------------- 1 | namespace CSharp.UnionTypes 2 | 3 | open FParsec 4 | 5 | [] 6 | module Parser = 7 | let ws p = spaces >>. p .>> spaces 8 | let word : Parser = ws (many1Chars asciiLetter) 9 | 10 | let wchar : char -> Parser = 11 | pchar 12 | >> ws 13 | >> attempt 14 | 15 | let wstr : string -> Parser = 16 | pstring 17 | >> ws 18 | >> attempt 19 | 20 | let braced p = wstr "{" >>. p .>> wstr "}" 21 | let pointed p = wstr "<" >>. p .>> wstr ">" 22 | let comma = wstr "," 23 | let pipe = wstr "|" 24 | let identifier = 25 | spaces >>. regex "[\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Lm}_][\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Lm}\d_]*[?]?" .>> spaces 26 | let dotComponent : Parser = (pchar '.') >>. identifier .>> spaces 27 | let fullTypeName, fullTypeNameImpl = createParserForwardedToRef() 28 | let typeArguments = pointed (sepBy1 fullTypeName comma) 29 | let dottedName = spaces >>. identifier .>>. many dotComponent 30 | 31 | do fullTypeNameImpl := dottedName .>>. opt typeArguments |>> FullTypeName.apply 32 | 33 | let memberName = word |>> UnionMemberName 34 | let caseMemberArgOpt = pointed fullTypeName |> opt 35 | let caseMember = ((memberName .>>. caseMemberArgOpt) |> ws) |>> UnionMember.apply 36 | let caseMembers = sepBy1 caseMember pipe 37 | let caseMembersBlock = braced caseMembers 38 | let typeParameters = (sepBy1 word comma |> pointed) |>> List.map TypeArgument 39 | let constrainsOpt = ((wstr "constrains") >>. fullTypeName) |> opt 40 | let unionTypeName = word |>> UnionTypeName 41 | let unionType = 42 | (wstr "union" >>. unionTypeName) .>>. (opt typeParameters) .>>. constrainsOpt .>>. caseMembersBlock 43 | .>> opt (wstr ";") |>> (UnionType.apply >> UnionType) "Union" 44 | let usingName = dottedName |>> UsingName.apply 45 | let using = wstr "using" >>. usingName .>> wstr ";" |>> Using "Using" 46 | let namespaceName = dottedName |>> NamespaceName.apply 47 | let namespaceMember = using <|> unionType 48 | let ``namespace`` = 49 | wstr "namespace" >>. namespaceName .>>. (namespaceMember |> (many >> braced)) |>> Namespace.apply 50 | "Namespace" 51 | 52 | let parseTextToNamespace str = 53 | match run ``namespace`` str with 54 | | Success(result, _, _) -> result 55 | | Failure(err, _, _) -> sprintf "Failure:%s[%s]" str err |> failwith 56 | 57 | let parse_namespace_from_text str = 58 | match run ``namespace`` str with 59 | | Success(result, _, _) -> Some result 60 | | _ -> None 61 | -------------------------------------------------------------------------------- /src/CSharp.UnionTypes.SourceGenerator/SourceGenerator.fs: -------------------------------------------------------------------------------- 1 | namespace CSharp.UnionTypes 2 | 3 | open Microsoft.CodeAnalysis 4 | open Microsoft.CodeAnalysis.Text 5 | open System.Text 6 | open System.IO 7 | open System.Threading 8 | 9 | module SourceGenerator = 10 | [] 11 | type FileTransformGenerator() = 12 | interface IIncrementalGenerator with 13 | member _.Initialize (context : IncrementalGeneratorInitializationContext) = 14 | let generateUnion (text: AdditionalText) (cancellationToken: CancellationToken) = 15 | let name = 16 | Path.GetFileName text.Path 17 | let code = 18 | text.GetText cancellationToken 19 | |> _.ToString() 20 | |> GenerateNamespaceCode 21 | in (name, code) 22 | 23 | let writeSourceFile (source : SourceProductionContext) ((name:string), (code:string)) = 24 | source.AddSource ($"{name}.generated.cs", SourceText.From(code, Encoding.UTF8)) 25 | 26 | let pipeline = 27 | context 28 | .AdditionalTextsProvider 29 | .Where(fun text -> text.Path.EndsWith (".csunion")) 30 | .Select(generateUnion) 31 | 32 | context.RegisterSourceOutput (pipeline, writeSourceFile) 33 | -------------------------------------------------------------------------------- /src/CSharp.UnionTypes.TestApplication/CSharp.UnionTypes.TestApplication.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8 5 | enable 6 | 10.0 7 | Exe 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/CSharp.UnionTypes.TestApplication/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace CSharp.UnionTypes.TestApplication 4 | { 5 | public static class Program 6 | { 7 | public static void Main (string[] args) 8 | { 9 | Maybe m23 = new Maybe.Some(23); 10 | 11 | var str = m23 switch 12 | { 13 | Maybe.Some { Value: var v } => $"Some {v}", 14 | Maybe.None => "None", 15 | _ => throw new NotImplementedException() 16 | }; 17 | 18 | Console.WriteLine(str); 19 | Console.WriteLine($"{m23}"); 20 | 21 | var red = new TrafficLights.Red(); 22 | var stopRed = (TrafficLightsToStopFor.Red)red; 23 | Console.WriteLine($"{red}, {stopRed}, {(TrafficLights.Red)stopRed}"); 24 | 25 | var card = new PaymentMethod.Card("1234"); 26 | AuditablePaymentMethod.Card auditableCard = card; 27 | Console.WriteLine($"{card}, {auditableCard}, {(PaymentMethod.Card)auditableCard}"); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/CSharp.UnionTypes.TestApplication/maybe.csunion: -------------------------------------------------------------------------------- 1 | namespace CSharp.UnionTypes.TestApplication 2 | { 3 | using System; 4 | 5 | union Maybe { Some | None }; 6 | 7 | union PaymentMethod { Cash | Card | Cheque }; 8 | union AuditablePaymentMethod constrains PaymentMethod { Card | Cheque }; 9 | 10 | union TrafficLights { Red | Amber | Green }; 11 | union TrafficLightsToStopFor constrains TrafficLights { Red | Amber }; 12 | } -------------------------------------------------------------------------------- /src/Key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnazariah/csharp-uniontypes/3e8b1f654562462aff7a0801cdbfd0099d23ef67/src/Key.snk -------------------------------------------------------------------------------- /tests/Tests.CSharp.UnionTypes.SourceGenerator/CodeEmitterTests.fs: -------------------------------------------------------------------------------- 1 | module CSharp.UnionTypes.SourceGenerator.CodeEmitterTests 2 | 3 | open CSharp.UnionTypes.CodeEmitter 4 | open Xunit 5 | 6 | [] 7 | let ``Maybe is generated correctly`` () = 8 | let actual = 9 | """namespace CSharp.UnionTypes.TestApplication 10 | { 11 | union Maybe { Some | None }; 12 | }""" 13 | |> GenerateNamespaceCode 14 | 15 | let expected = 16 | """namespace CSharp.UnionTypes.TestApplication 17 | { 18 | public abstract partial record Maybe 19 | { 20 | private Maybe() { } 21 | public sealed partial record Some(T Value) : Maybe 22 | { 23 | override public string ToString() => $"Maybe<{typeof(T)}>.Some {Value}"; 24 | } 25 | public sealed partial record None() : Maybe 26 | { 27 | override public string ToString() => $"Maybe<{typeof(T)}>.None"; 28 | } 29 | } 30 | } 31 | """ 32 | Assert.Equal (expected.Replace("\r\n", "\n"), actual.Replace("\r\n", "\n")) 33 | 34 | [] 35 | let ``Constrains clause for enumerations is generated correctly`` () = 36 | let actual = 37 | """namespace CSharp.UnionTypes.TestApplication 38 | { 39 | union TrafficLights { Red | Amber | Green }; 40 | union TrafficLightsToStopFor constrains TrafficLights { Red | Amber }; 41 | }""" 42 | |> GenerateNamespaceCode 43 | 44 | let expected = 45 | """namespace CSharp.UnionTypes.TestApplication 46 | { 47 | public abstract partial record TrafficLights 48 | { 49 | private TrafficLights() { } 50 | public sealed partial record Red() : TrafficLights 51 | { 52 | override public string ToString() => $"TrafficLights.Red"; 53 | } 54 | public sealed partial record Amber() : TrafficLights 55 | { 56 | override public string ToString() => $"TrafficLights.Amber"; 57 | } 58 | public sealed partial record Green() : TrafficLights 59 | { 60 | override public string ToString() => $"TrafficLights.Green"; 61 | } 62 | } 63 | public abstract partial record TrafficLightsToStopFor 64 | { 65 | private TrafficLightsToStopFor() { } 66 | public sealed partial record Red() : TrafficLightsToStopFor 67 | { 68 | override public string ToString() => $"TrafficLightsToStopFor.Red"; 69 | public static implicit operator Red(TrafficLights.Red value) => new Red(); 70 | public static implicit operator TrafficLights.Red(Red value) => new TrafficLights.Red(); 71 | } 72 | public sealed partial record Amber() : TrafficLightsToStopFor 73 | { 74 | override public string ToString() => $"TrafficLightsToStopFor.Amber"; 75 | public static implicit operator Amber(TrafficLights.Amber value) => new Amber(); 76 | public static implicit operator TrafficLights.Amber(Amber value) => new TrafficLights.Amber(); 77 | } 78 | } 79 | } 80 | """ 81 | Assert.Equal (expected.Replace("\r\n", "\n"), actual.Replace("\r\n", "\n")) 82 | 83 | 84 | [] 85 | let ``Constrains clause for value constructed unions is generated correctly`` () = 86 | let actual = 87 | """namespace CSharp.UnionTypes.TestApplication 88 | { 89 | union PaymentMethod { Cash | Card | Cheque }; 90 | union AuditablePaymentMethod constrains PaymentMethod { Card | Cheque }; 91 | }""" 92 | |> GenerateNamespaceCode 93 | 94 | let expected = 95 | """namespace CSharp.UnionTypes.TestApplication 96 | { 97 | public abstract partial record PaymentMethod 98 | { 99 | private PaymentMethod() { } 100 | public sealed partial record Cash() : PaymentMethod 101 | { 102 | override public string ToString() => $"PaymentMethod<{typeof(TCard)}, {typeof(TCheque)}>.Cash"; 103 | } 104 | public sealed partial record Card(TCard Value) : PaymentMethod 105 | { 106 | override public string ToString() => $"PaymentMethod<{typeof(TCard)}, {typeof(TCheque)}>.Card {Value}"; 107 | } 108 | public sealed partial record Cheque(TCheque Value) : PaymentMethod 109 | { 110 | override public string ToString() => $"PaymentMethod<{typeof(TCard)}, {typeof(TCheque)}>.Cheque {Value}"; 111 | } 112 | } 113 | public abstract partial record AuditablePaymentMethod 114 | { 115 | private AuditablePaymentMethod() { } 116 | public sealed partial record Card(TCard Value) : AuditablePaymentMethod 117 | { 118 | override public string ToString() => $"AuditablePaymentMethod<{typeof(TCard)}, {typeof(TCheque)}>.Card {Value}"; 119 | public static implicit operator Card(PaymentMethod.Card value) => new Card(value.Value); 120 | public static implicit operator PaymentMethod.Card(Card value) => new PaymentMethod.Card(value.Value); 121 | } 122 | public sealed partial record Cheque(TCheque Value) : AuditablePaymentMethod 123 | { 124 | override public string ToString() => $"AuditablePaymentMethod<{typeof(TCard)}, {typeof(TCheque)}>.Cheque {Value}"; 125 | public static implicit operator Cheque(PaymentMethod.Cheque value) => new Cheque(value.Value); 126 | public static implicit operator PaymentMethod.Cheque(Cheque value) => new PaymentMethod.Cheque(value.Value); 127 | } 128 | } 129 | } 130 | """ 131 | Assert.Equal (expected.Replace("\r\n", "\n"), actual.Replace("\r\n", "\n")) -------------------------------------------------------------------------------- /tests/Tests.CSharp.UnionTypes.SourceGenerator/ParserTests.fs: -------------------------------------------------------------------------------- 1 | module CSharp.UnionTypes.SourceGenerator.ParserTests 2 | 3 | open CSharp.UnionTypes 4 | open Xunit 5 | open FParsec 6 | 7 | let private test p str = 8 | match run p str with 9 | | Success(result, _, posn) -> 10 | printfn "Success:%s=>%O" str result 11 | true 12 | | Failure(err, state, _) -> 13 | printfn "Failure:%s[%s]" str err 14 | false 15 | 16 | let private AssertIsValid p s = test p s |> Assert.True 17 | let private AssertIsInvalid p s = test p s |> Assert.False 18 | 19 | let private AssertParsesTo p str expected = 20 | match run p str with 21 | | Success(result, _, posn) -> 22 | let result' = result.ToString() 23 | printfn "Success:%s=>%O" str result 24 | Assert.Equal(result', expected) 25 | | Failure(err, state, _) -> 26 | printfn "Failure:%s[%s]" str err 27 | Assert.Fail "Parse Failed" 28 | 29 | let private AssertIsValidUnion = AssertIsValid unionType 30 | let private AssertIsInvalidUnion = AssertIsInvalid unionType 31 | 32 | [] 33 | let ``parser: case class parses``() = 34 | let input = "Result" 35 | let parser = caseMember 36 | AssertParsesTo parser input "Result of T" 37 | 38 | [] 39 | let ``parser: case object parses``() = 40 | let input = "Exception;" 41 | let parser = caseMember 42 | AssertParsesTo parser input "Exception" 43 | 44 | [] 45 | let ``parser: type - simple name``() = 46 | let input = "String" 47 | AssertIsValid fullTypeName input 48 | 49 | [] 50 | let ``parser: type - name with embedded digits``() = 51 | let input = "Int32" 52 | AssertParsesTo fullTypeName input "Int32" 53 | 54 | [] 55 | let ``parser: type - name with leading digits``() = 56 | let input = "2B" 57 | AssertIsInvalid fullTypeName input 58 | 59 | [] 60 | let ``parser: type - name with embedded _``() = 61 | let input = "This_Is_Good" 62 | AssertParsesTo fullTypeName input "This_Is_Good" 63 | 64 | [] 65 | let ``parser: type - name with leading _``() = 66 | let input = "_this_is_also_good" 67 | AssertParsesTo fullTypeName input "_this_is_also_good" 68 | 69 | [] 70 | let ``parser: type - name with trailing ?``() = 71 | let input = "int?" 72 | AssertParsesTo fullTypeName input "int?" 73 | 74 | [] 75 | let ``parser: type - dotted name``() = 76 | let input = "System.String" 77 | AssertParsesTo fullTypeName input "System.String" 78 | 79 | [] 80 | let ``parser: type - generic name``() = 81 | let input = "List" 82 | AssertParsesTo fullTypeName input "List" 83 | 84 | [] 85 | let ``parser: type - generic name with dotted type argument``() = 86 | let input = "List" 87 | AssertParsesTo fullTypeName input "List" 88 | 89 | [] 90 | let ``parser: type - dotted generic name``() = 91 | let input = "System.Collections.Generic.List" 92 | AssertParsesTo fullTypeName input "System.Collections.Generic.List" 93 | 94 | [] 95 | let ``parser: type - dotted generic name with multiple type arguments``() = 96 | let input = "System.Collections.Generic.Dictionary" 97 | AssertParsesTo fullTypeName input "System.Collections.Generic.Dictionary" 98 | 99 | [] 100 | let ``parser: type - dotted generic name with multiple fully qualified type arguments``() = 101 | let input = "System.Collections.Generic.Dictionary" 102 | AssertParsesTo fullTypeName input "System.Collections.Generic.Dictionary" 103 | 104 | [] 105 | let ``parser: type - nested generic``() = 106 | let input = "Something.Lazy>" 107 | AssertParsesTo fullTypeName input "Something.Lazy>" 108 | 109 | [] 110 | let ``parser: union - non-generic union parses``() = 111 | let input = @" union TrafficLight { Red | Amber | Green }"; 112 | AssertParsesTo unionType input "union TrafficLight ::= [ Red | Amber | Green ]" 113 | 114 | [] 115 | let ``parser: union - invalid non-generic union does not parse``() = 116 | let input = @" 117 | union TrafficLight[A] 118 | { 119 | Red 120 | | Amber 121 | | Green 122 | }" 123 | AssertIsInvalidUnion input 124 | 125 | [] 126 | let ``parser: union - generic hybrid union parses``() = 127 | let input = @"union Maybe { Some | None }"; 128 | AssertParsesTo unionType input "union Maybe ::= [ Some of T | None ]" 129 | 130 | [] 131 | let ``parser: union - total generic union - one argument per case-class``() = 132 | let input = @" 133 | union Either 134 | { 135 | Left | Right 136 | }" 137 | AssertParsesTo unionType input "union Either ::= [ Left of L | Right of R ]" 138 | 139 | [] 140 | let ``parser: union - total generic union - cannot have more than one generic argument per case-class``() = 141 | let input = @" 142 | union Either 143 | { 144 | Left | Right 145 | }" 146 | AssertIsInvalidUnion input 147 | 148 | [] 149 | let ``parser: union - total generic union - generic type enclosing type argument``() = 150 | let input = @"union Either { Left> | Right }" 151 | AssertParsesTo unionType input "union Either ::= [ Left of List | Right of R ]" 152 | 153 | [] 154 | let ``parser: union - union generic types - union may contain arguments from only some constituent case classes``() = 155 | let input = @" 156 | union Result 157 | { 158 | Result | Error 159 | }" 160 | AssertParsesTo unionType input "union Result ::= [ Result of T | Error of Exception ]" 161 | 162 | [] 163 | let ``parser: union - fully qualified types can be used as case class arguments``() = 164 | let input = @"union Result { Result | Error }" 165 | AssertParsesTo unionType input "union Result ::= [ Result of T | Error of System.Exception ]" 166 | 167 | [] 168 | let ``parser: union - union generic types - union may contain superfluous arguments``() = 169 | let input = @" 170 | union Either 171 | { 172 | Left | Right 173 | }" 174 | AssertParsesTo unionType input "union Either ::= [ Left of L | Right of R ]" 175 | 176 | [] 177 | let ``parser: union - non generic union may have case class members``() = 178 | let input = @"union Payment { Cash | CreditCard | Cheque }" 179 | AssertParsesTo unionType input 180 | "union Payment ::= [ Cash | CreditCard of CreditCardDetails | Cheque of ChequeDetails ]" 181 | 182 | [] 183 | let ``parser: using - simple case``() = 184 | let input = @" 185 | using System.Collections.Generic; 186 | " 187 | AssertParsesTo using input "System.Collections.Generic" 188 | 189 | [] 190 | let ``parser: namespace - empty``() = 191 | let input = @" 192 | namespace CoolMonads 193 | { 194 | } 195 | " 196 | AssertParsesTo ``namespace`` input "namespace CoolMonads{}" 197 | 198 | [] 199 | let ``parser: namespace - dotted name``() = 200 | let input = @" 201 | namespace DU.Tests 202 | { 203 | } 204 | " 205 | AssertParsesTo ``namespace`` input "namespace DU.Tests{}" 206 | 207 | [] 208 | let ``parser: namespace - single using and union``() = 209 | let input = @" 210 | namespace CoolMonads 211 | { 212 | using System; 213 | 214 | union Payment 215 | { 216 | Cash 217 | | CreditCard 218 | | Cheque 219 | } 220 | } 221 | " 222 | let expected = 223 | "namespace CoolMonads{System; Payment}" 224 | AssertParsesTo ``namespace`` input expected 225 | 226 | [] 227 | let ``parser: namespace - multiple using and union``() = 228 | let input = @" 229 | namespace CoolMonads 230 | { 231 | using System; 232 | using System.Collections.Generic; 233 | 234 | union Payment 235 | { 236 | Cash 237 | | CreditCard 238 | | Cheque 239 | } 240 | 241 | union Result { Result | Error } 242 | } 243 | " 244 | let expected = 245 | "namespace CoolMonads{System; System.Collections.Generic; Payment; Result}" 246 | AssertParsesTo ``namespace`` input expected 247 | -------------------------------------------------------------------------------- /tests/Tests.CSharp.UnionTypes.SourceGenerator/SourceGeneratorTests.fs: -------------------------------------------------------------------------------- 1 | module CSharp.UnionTypes.SourceGenerator.SourceGeneratorTests 2 | open CSharp.UnionTypes.SourceGenerator 3 | 4 | -------------------------------------------------------------------------------- /tests/Tests.CSharp.UnionTypes.SourceGenerator/Tests.CSharp.UnionTypes.SourceGenerator.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | false 6 | false 7 | true 8 | 9 | 10 | 11 | 12 | Never 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | all 33 | 34 | 35 | runtime; build; native; contentfiles; analyzers; buildtransitive 36 | all 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | --------------------------------------------------------------------------------