├── .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 |
--------------------------------------------------------------------------------