Skip to content

Commit

Permalink
Johnaz/add constrains clause (#31)
Browse files Browse the repository at this point in the history
* clean up index page

* draft tutorial.md

* add constrains clause
  • Loading branch information
johnazariah authored Jul 24, 2024
1 parent b92c00b commit fc97e9a
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 73 deletions.
6 changes: 6 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#### 1.0.1 - Jul 15 2024
* Renamed package to JohnAz.CSharp.UnionTypes
* Use modern (C# 9+) idioms with `record` types to provide value semantics and pattern matching
* Use GitVersion for semantic versioning

#### 1.0.1 - Jan 10 2017
* SingleFileGenerator and VSIX also published
* Support for constrained types
Expand All @@ -6,6 +11,7 @@
* Initial release of C# Discriminated Union types
* Parser and Roslyn-Based Code Generator library for Discriminated Unions
* Command Line Executable

#### 0.0.1-beta - Dec 13 2016
* Changed name from fsharp-project-scaffold to CSharp.UnionTypes
* Initial release
15 changes: 7 additions & 8 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,10 @@ The JohnAz.CSharp.UnionTypes library can be [installed from NuGet](https://www.n

<pre>PM> NuGet\Install-Package JohnAz.CSharp.UnionTypes</pre>

### Example
### Use
-------

Unions are defined in `.csunion` files using a little DSL, which are 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#.

Consider a `.csunion` file containing the following text:

* 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:
```c++

namespace Monads
Expand All @@ -25,9 +22,11 @@ namespace Monads
}
```

This signifies that we want a type called `Maybe<T>` which can be _either_ a value of type `Some<T>` or of `None`.
This indicates that a `Maybe<T>` type is _either_ a `Some` wrapping a value of type `T`, or a `None` "marker" value.

* 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#.

Using value semantics provided by records in C# 10.0 and above, we can then generate the following code which implements the discriminated union.
Using value semantics provided by records in C# 9.0 and above, we can then generate the following code which implements the discriminated union.

```csharp
namespace Monads
Expand All @@ -41,7 +40,7 @@ namespace Monads
}
```

Then, when we wanted to use the `Maybe<T>` type in our code, we could make use of [switch expressions](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/switch-expression) to properly handle the various cases.
* We can then use the `Maybe<T>` 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.

```csharp
public static void Main (string[] args)
Expand Down
66 changes: 5 additions & 61 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,9 @@ Union Types for C#

## Summary

This project provides a tool-based solution to allow modelling of union types within a C# project.
This library provides a source-generator based solution to allow modelling of union types within a C# project.

It defines a minimal extension to the C# language, and provides a CustomTool to automatically generate idiomatic C# classes which provide the functionality of union types.

<!-- ## Usage Instructions for Visual Studio 2015
This project provides a VSIX for use with Visual Studio 2015. You can also find this [VSIX at the Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=JohnAzariah.CUnionTypes).
* Install the VSIX into your working environment
* Define your union types in a file with extension `.csunion`.
The VSIX contains a "CustomTool" (also known as a Single File Generator) called "CSharpUnionTypeGenerator". This generates "code-behind" C# for your `.csunion` file.
* Create a file to contain your union types in your Visual Studio C# Project.
In the file properties window, ensure that:
* **Build Action** is set to _Content_
* **Copy to Output Directory** is set to _Never_
* **Custom Tool** is set to _CSharpUnionTypeGenerator_
Whenever you save the `.csunion` file, a `.g.cs` file - which is its "code-behind" - is generated. It will automatically be added to your project.
* Write more C# code (in traditional `.cs` files) using the union types.
* Compile the project as usual
## Manual Usage Instructions for other environments
This project provides a command-line version of the tool packaged in the Nuget package. This executable is called 'csutc.exe'.
* Download the Nuget package and update your PATH to have 'csutc.exe' accessible
* Define your union types in a file with extension `.csunion`.
* Run `csutc.exe --input-file=<full-path-to-your-file>.csunion`. You will need to do this every time you change the `.csunion` file's contents.
The command-line executable will generate a `.cs` file with the same name at the same location by default.
* Add this C# file to your C# Project.
* You may also add the `.csunion` file to your project, but ensure that its properties are set as follows:
* **Build Action** is set to _Content_
* **Copy to Output Directory** is set to _Never_
* **Custom Tool** is set to _CSharpUnionTypeGenerator_
* Write more C# code (in traditional `.cs` files) using the union types.
* Compile the project as usual -->
It defines a minimal extension to the C# language, and automatically generate idiomatic C# classes which provide the functionality of union types.

## Structure of a .csunion file

Expand Down Expand Up @@ -141,7 +101,7 @@ _Note that in this case, one or more `using` directives including the assembly (
union Either<L, R> { Left<L> | Right<R> }
```
This discriminated union demonstrates multiple type parameters.
<!--

#### Constrained Types
```
union TrafficLightsToStopFor constrains TrafficLights { Red | Amber }
Expand All @@ -150,7 +110,7 @@ Typically, classes are specified with base functionality, which can be augmented

The `constrains` keyword allows for such a specification.

* **It is illegal to specify a member in a constrained type that does not exist in the type it is constraining.** -->
* **It is illegal to specify a member in a constrained type that does not exist in the type it is constraining.**

## How to code against a union type

Expand All @@ -175,20 +135,6 @@ For value constructor choices, you will need to provide the value to the constru
```
var name = new Maybe<string>.Some("John");
```
<!--
### Pattern Matching
Given an instance of the Union Type, one may wish to discriminate between the various choices and extract any wrapped values.
One of the primary benefits of using Union Types is to provide safety - to always ensure that all possible options are handled, for example. Therefore, we do not provide a way to enumerate over the choices with `switch` or `if-then-else` statements.
Instead, each Union Type defines a `Match` function, which takes lambdas for each of the choices and invokes the appropriate function. In this way, modifying the Union enforces the appropriate updates in _all_ the places where the Union is used.
Given the `name` definition above, we can get the wrapped value (or `String.Empty` if it isn't available) by:
```
var value = name.Match(() => String.Empty, v => v);
```

### Augmenting the partial class

Expand Down Expand Up @@ -303,6 +249,4 @@ This is far more precise than a record which may introduce illegal states where

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.

Alternately, one could use this project to model union-types without switching languages.
*)-->
Alternately, one could use this library to model union-types without switching languages.
12 changes: 11 additions & 1 deletion src/CSharp.UnionTypes.SourceGenerator/AST.fs
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,21 @@ module AST =
in
sprintf "%s%s" bareTypeName typeParameters

member this.ValueMember =
match this.MemberArgumentType with
| Some _ -> "Value"
| None -> ""

member this.UnionMemberValueMember =
match this.MemberArgumentType with
| Some mat -> sprintf "(%s Value)" mat.CSharpTypeName
| Some mat -> sprintf "(%s %s)" mat.CSharpTypeName this.ValueMember
| None -> "()"

member this.UnionMemberValueAccessor(varName) =
match this.MemberArgumentType with
| Some _ -> sprintf "%s.%s" varName this.ValueMember
| None -> ""

override this.ToString() =
this.MemberArgumentType
|> Option.fold (fun _ s -> sprintf "%s of %s" this.MemberName.unapply (s.ToString()))
Expand Down
9 changes: 9 additions & 0 deletions src/CSharp.UnionTypes.SourceGenerator/CodeEmitter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,18 @@ module CodeEmitter =
| Some _ -> $" {{Value}}"
| None -> ""
indentAndWriteLine $"override public string ToString() => $\"{union.UnionClassNameWithTypeofTypeArgs}.{unionMember.MemberName.unapply}{memberValuePattern}\";"
match union.BaseType with
| Some baseType ->
let valueAccessor = unionMember.UnionMemberValueAccessor("value")
indentAndWriteLine $"public static implicit operator {unionMember.MemberName.unapply}({baseType.CSharpTypeName}.{unionMember.MemberName.unapply} value) => new {unionMember.MemberName.unapply}({valueAccessor});"
indentAndWriteLine $"public static implicit operator {baseType.CSharpTypeName}.{unionMember.MemberName.unapply}({unionMember.MemberName.unapply} value) => new {baseType.CSharpTypeName}.{unionMember.MemberName.unapply}({valueAccessor});"
| None ->
()

indentWriter.Indent <- indentWriter.Indent - 1
indentAndWriteLine $"}}"


indentAndWriteLine $"public abstract partial record {union.UnionClassNameWithTypeArgs}"
indentAndWriteLine $"{{"

Expand Down
10 changes: 8 additions & 2 deletions src/CSharp.UnionTypes.TestApplication/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ namespace CSharp.UnionTypes.TestApplication
{
public static class Program
{

public static void Main (string[] args)
{
Maybe<int> m23 = new Maybe<int>.Some(23);
Expand All @@ -18,7 +17,14 @@ public static void Main (string[] args)

Console.WriteLine(str);
Console.WriteLine($"{m23}");
Console.WriteLine($"{new Result<int, Exception>.Return(18)}");

var red = new TrafficLights.Red();
var stopRed = (TrafficLightsToStopFor.Red)red;
Console.WriteLine($"{red}, {stopRed}, {(TrafficLights.Red)stopRed}");

var card = new PaymentMethod<string, int>.Card("1234");
AuditablePaymentMethod<string, int>.Card auditableCard = card;
Console.WriteLine($"{card}, {auditableCard}, {(PaymentMethod<string, int>.Card)auditableCard}");
}
}
}
7 changes: 6 additions & 1 deletion src/CSharp.UnionTypes.TestApplication/maybe.csunion
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
{
using System;

union Result<V,E> { Return<V> | Error<E> };
union Maybe<T> { Some<T> | None };

union PaymentMethod<TCard, TCheque> { Cash | Card<TCard> | Cheque<TCheque> };
union AuditablePaymentMethod<TCard, TCheque> constrains PaymentMethod<TCard, TCheque> { Card<TCard> | Cheque<TCheque> };

union TrafficLights { Red | Amber | Green };
union TrafficLightsToStopFor constrains TrafficLights { Red | Amber };
}
99 changes: 99 additions & 0 deletions tests/Tests.CSharp.UnionTypes.SourceGenerator/CodeEmitterTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,104 @@ let ``Maybe<T> is generated correctly`` () =
}
}
}
"""
Assert.Equal (expected.Replace("\r\n", "\n"), actual.Replace("\r\n", "\n"))

[<Fact>]
let ``Constrains clause for enumerations is generated correctly`` () =
let actual =
"""namespace CSharp.UnionTypes.TestApplication
{
union TrafficLights { Red | Amber | Green };
union TrafficLightsToStopFor constrains TrafficLights { Red | Amber };
}"""
|> GenerateNamespaceCode

let expected =
"""namespace CSharp.UnionTypes.TestApplication
{
public abstract partial record TrafficLights
{
private TrafficLights() { }
public sealed partial record Red() : TrafficLights
{
override public string ToString() => $"TrafficLights.Red";
}
public sealed partial record Amber() : TrafficLights
{
override public string ToString() => $"TrafficLights.Amber";
}
public sealed partial record Green() : TrafficLights
{
override public string ToString() => $"TrafficLights.Green";
}
}
public abstract partial record TrafficLightsToStopFor
{
private TrafficLightsToStopFor() { }
public sealed partial record Red() : TrafficLightsToStopFor
{
override public string ToString() => $"TrafficLightsToStopFor.Red";
public static implicit operator Red(TrafficLights.Red value) => new Red();
public static implicit operator TrafficLights.Red(Red value) => new TrafficLights.Red();
}
public sealed partial record Amber() : TrafficLightsToStopFor
{
override public string ToString() => $"TrafficLightsToStopFor.Amber";
public static implicit operator Amber(TrafficLights.Amber value) => new Amber();
public static implicit operator TrafficLights.Amber(Amber value) => new TrafficLights.Amber();
}
}
}
"""
Assert.Equal (expected.Replace("\r\n", "\n"), actual.Replace("\r\n", "\n"))


[<Fact>]
let ``Constrains clause for value constructed unions is generated correctly`` () =
let actual =
"""namespace CSharp.UnionTypes.TestApplication
{
union PaymentMethod<TCard, TCheque> { Cash | Card<TCard> | Cheque<TCheque> };
union AuditablePaymentMethod<TCard, TCheque> constrains PaymentMethod<TCard, TCheque> { Card<TCard> | Cheque<TCheque> };
}"""
|> GenerateNamespaceCode

let expected =
"""namespace CSharp.UnionTypes.TestApplication
{
public abstract partial record PaymentMethod<TCard, TCheque>
{
private PaymentMethod() { }
public sealed partial record Cash() : PaymentMethod<TCard, TCheque>
{
override public string ToString() => $"PaymentMethod<{typeof(TCard)}, {typeof(TCheque)}>.Cash";
}
public sealed partial record Card(TCard Value) : PaymentMethod<TCard, TCheque>
{
override public string ToString() => $"PaymentMethod<{typeof(TCard)}, {typeof(TCheque)}>.Card {Value}";
}
public sealed partial record Cheque(TCheque Value) : PaymentMethod<TCard, TCheque>
{
override public string ToString() => $"PaymentMethod<{typeof(TCard)}, {typeof(TCheque)}>.Cheque {Value}";
}
}
public abstract partial record AuditablePaymentMethod<TCard, TCheque>
{
private AuditablePaymentMethod() { }
public sealed partial record Card(TCard Value) : AuditablePaymentMethod<TCard, TCheque>
{
override public string ToString() => $"AuditablePaymentMethod<{typeof(TCard)}, {typeof(TCheque)}>.Card {Value}";
public static implicit operator Card(PaymentMethod<TCard, TCheque>.Card value) => new Card(value.Value);
public static implicit operator PaymentMethod<TCard, TCheque>.Card(Card value) => new PaymentMethod<TCard, TCheque>.Card(value.Value);
}
public sealed partial record Cheque(TCheque Value) : AuditablePaymentMethod<TCard, TCheque>
{
override public string ToString() => $"AuditablePaymentMethod<{typeof(TCard)}, {typeof(TCheque)}>.Cheque {Value}";
public static implicit operator Cheque(PaymentMethod<TCard, TCheque>.Cheque value) => new Cheque(value.Value);
public static implicit operator PaymentMethod<TCard, TCheque>.Cheque(Cheque value) => new PaymentMethod<TCard, TCheque>.Cheque(value.Value);
}
}
}
"""
Assert.Equal (expected.Replace("\r\n", "\n"), actual.Replace("\r\n", "\n"))

0 comments on commit fc97e9a

Please sign in to comment.