diff --git a/README.md b/README.md index 5fbb927..dbef4c3 100644 --- a/README.md +++ b/README.md @@ -2,37 +2,70 @@ [![CI](https://github.com/mattwar/UnionTypes.Toolkit/actions/workflows/ci.yml/badge.svg)](https://github.com/mattwar/UnionTypes.Toolkit/actions/workflows/ci.yml) [![GitHub release](https://img.shields.io/github/v/release/mattwar/UnionTypes.Toolkit)](https://github.com/mattwar/UnionTypes.Toolkit/releases/latest) -[![NuGet](https://img.shields.io/nuget/v/UnionTypes.Toolkit.Generator)](https://www.nuget.org/packages/UnionTypes.Toolkit.Generator) -This repo contains a library implementing some common custom union types compatible with the C# union type feature and a source generator that generates custom C# union types that uses techniques to avoid boxing and minimize memory footprint. +This repo is the source beind two packages available on Nuget. -This library was originally one of many similar libraries I created when designing and working on the C# Union Types feature. It has now been refitted to match the shipping design and the generator reimplemented to be more robust. +- [UnionTypes.Toolkit](#uniontypestoolkit) - a library of common custom union types + [![NuGet](https://img.shields.io/nuget/v/UnionTypes.Toolkit.Generator)](https://www.nuget.org/packages/UnionTypes.Toolkit.Generator) -Please report bugs here under issues or submit PRs to fix them if you prefer. +- [UnionTypes.Toolkit.Generator](#uniontypestoolkitgenerator) - a source generator for non-boxing custom unions + [![NuGet](https://img.shields.io/nuget/v/UnionTypes.Toolkit)](https://www.nuget.org/packages/UnionTypes.Toolkit) + + +### What is this? + +This repo was originally created as a design playground when working on the C# Union Types feature. It was meant to contain various hand built union types with a variety of implementation strategies and a general configurable source generator that would produce custom unions using the techniques being discussed during design meetings. +This was long before any actual design was settled on and language feature worked started. + +The original nuget release years ago contained a very different concept of unions to what has now become a feature for C# 15. + +Both the toolkit library and the source generator have been updated to match the new C# Union Types feature, and both are now available for general use. + +The source generator has been retrofitted to produce only unions matching the specification for C# Union Types feature and using the Custom Unions API's. +The generator produces unions that do not box struct values by default. + +### How to Contribute + +Please report bugs here under issues or submit PRs to fix them if you prefer. Use discussions instead of issues to share ideas or make requests. +---- +
-## The UnionTypes.Toolkit Library +## UnionTypes.Toolkit -**This is a work in progress** +A collection of common union types compatible with the C# Union Types feature. -It currently includes implementations of Option, Result and a family of generic 'boxed' unions, for use when you don't need the formality of inventing a new named union. There is also a family of non-boxing (fat) generic unions for when you don't like boxing and are not concerned with memory footprint. +**Option** -*This library is not yet published to NuGet, but can be accessed from releases on GitHub.* +The `Option` union type allows you to represent either `Some` or `None` without boxing. +**Result** -## The UnionTypes.Toolkit.Generator Library +The `Result` union type allows you to represent either `Success` or `Failure` without boxing. -This library implements a C# source generator for generating non-boxing custom union types compatible with the C# union types feature. It may contain additional generators in the future. +**Union** -The generator is purely standalone; the generated union source does not depend on the union type library or any other external library beyond the standard dotnet runtime to function. +A family of generic union types `Union`, `Union`, etc. + +You can use these without declaring a unique named union type. +The held value will be boxed, however. + +**FatUnion** -### Download the Generator +A family of generic union types `FatUnion`, `FatUnion`, etc. -The generator is available as a nuget package, or can be accessed from release builds here on GitHub. +You can use these without declaring a unique named union type. +The held value will not be boxed, but the type requires a memory footprint similar to using a tuple. -### [Download from Nuget Here](https://www.nuget.org/packages/UnionTypes.Toolkit.Generator) +---- +
+## UnionTypes.Toolkit.Generator + +This library implements a C# source generator for generating non-boxing custom union types compatible with the C# union types feature. It may contain additional generators in the future. + +The generator is purely standalone; the generated union source does not depend on the union type library or any other external library beyond the standard dotnet runtime to function. ## Declaring a Non-Boxing Custom Union Type diff --git a/src/SourceGenerators.Package/ReadMe.Nuget.md b/src/SourceGenerators.Package/ReadMe.Nuget.md index 76d8165..ec23215 100644 --- a/src/SourceGenerators.Package/ReadMe.Nuget.md +++ b/src/SourceGenerators.Package/ReadMe.Nuget.md @@ -1,4 +1,4 @@ -# UnionTypes.Toolkit.Generators +# UnionTypes.Toolkit.Generator A C# source generator library for generating custom union types compatible with the C# union types feature. @@ -35,4 +35,4 @@ The generator will layout the contents of the custom union so that the fields st In this example, there will be a single field storing a struct that contains enough space to store either an int, float, Coordinate or the address Id and a sparate object field used to store either a string, IManifest or the address Name. -# [Learn how to customize the union generation further](https://github.com/mattwar/UnionTypes.Toolkit) +[Learn how to customize the union generation further](https://github.com/mattwar/UnionTypes.Toolkit) diff --git a/src/UnionTypes.Tests/ResultTests.cs b/src/UnionTypes.Tests/ResultTests.cs index bab89a1..62d47dd 100644 --- a/src/UnionTypes.Tests/ResultTests.cs +++ b/src/UnionTypes.Tests/ResultTests.cs @@ -26,7 +26,7 @@ public void Test_Default_HasNoValue() { Result result = default; Assert.IsFalse(result.HasValue); - Assert.IsTrue(result.Value is null); + Assert.ThrowsException(() => { var value = result.Value; }); } [TestMethod] diff --git a/src/UnionTypes/Option.cs b/src/UnionTypes/Option.cs index e33d5b3..18889ab 100644 --- a/src/UnionTypes/Option.cs +++ b/src/UnionTypes/Option.cs @@ -1,44 +1,36 @@ namespace UnionTypes.Toolkit; +/// +/// A union that may contain either a value or a value. +/// It is similar to , except the value can be also be a reference type and may use null as a valid value. +/// [System.Runtime.CompilerServices.Union] public struct Option : System.Runtime.CompilerServices.IUnion { - private readonly object? _value; - - private static readonly bool _isNoneType = typeof(T) == typeof(None); + private readonly T _value; + private readonly bool _hasValue; public Option(Some value) { - if (_isNoneType) - { - // some cheeky user has used the None type as the value type. - _value = _someOfNoneBoxed; - } - else - { - _value = value.Value; - } + _value = value.Value; + _hasValue = true; } public Option(None value) { // store None as null, so it matches the same state as when the struct is default-initialized. - _value = null; + _value = default!; + _hasValue = false; } - public bool HasValue => false; // we return None if null, so HasValue is always false + public bool HasValue => true; // always has either some or none, so this is always true. public bool TryGetValue(out Some value) { - if (_value is T val) - { - value = new Some(val); - return true; - } - else if (_value is Some someValue) + if (_hasValue) { - value = someValue; + value = new Some(_value); return true; } else @@ -50,9 +42,9 @@ public bool TryGetValue(out Some value) public bool TryGetValue(out None value) { - if (_value == null) + if (!_hasValue) { - value = Option.None; + value = new None(); return true; } else @@ -62,17 +54,10 @@ public bool TryGetValue(out None value) } } - public object Value => _value switch - { - null => _noneBoxed, - T val => val, - _ => _value - }; - - public static implicit operator Option(T value) => new Option(new Some(value)); + public object Value => + _hasValue ? new Some(_value) : new None(); - private readonly object _noneBoxed = new None(); - private readonly object _someOfNoneBoxed = new Some(new None()); + public static implicit operator Option(T value) => new Option(new Some(value)); } /// diff --git a/src/UnionTypes/ReadMe.Nuget.md b/src/UnionTypes/ReadMe.Nuget.md index 5043bb2..3245326 100644 --- a/src/UnionTypes/ReadMe.Nuget.md +++ b/src/UnionTypes/ReadMe.Nuget.md @@ -1,5 +1,25 @@ # UnionTypes.Toolkit -A collection of common union types and tools for building custom ones. +A collection of common union types compatible with the C# Union Types feature. + +**Option** + +The `Option` union type allows you to represent either `Some` or `None`. + +**Result** + +The `Result` union type allows you to represent either `Success` or `Failure`. + +**Union** + +A family of generic union types `Union`, `Union`, etc. +You can use these without declaring a unique named union type. +The held value will be boxed, however. + +**FatUnion** + +A family of generic union types `FatUnion`, `FatUnion`, etc. +You can use these without declaring a unique named union type. +The held value will not be boxed, but the type has space for all cases, similar to a tuple. [Learn about using the toolkit in your project here.](https://github.com/mattwar/UnionTypes.Toolkit) diff --git a/src/UnionTypes/Result.cs b/src/UnionTypes/Result.cs index c22553e..7f66b83 100644 --- a/src/UnionTypes/Result.cs +++ b/src/UnionTypes/Result.cs @@ -3,67 +3,38 @@ namespace UnionTypes.Toolkit; +/// +/// A union that may contain either a value or a value. +/// [System.Runtime.CompilerServices.Union] public struct Result : System.Runtime.CompilerServices.IUnion { - private readonly object? _value; - - private static readonly bool _typeArgsMatch = - typeof(TValue).IsAssignableTo(typeof(TError)) - || typeof(TError).IsAssignableFrom(typeof(TValue)); + private readonly byte _kind; + private readonly TValue _value; + private readonly TError _error; public Result(Success value) { - if (_typeArgsMatch) - { - // both TValue and TError have intersecting types, so we must store the value as the Success boxed. - _value = value; - } - else if (value.Value == null) - { - // the success value is itself null, use the pre-boxed default. - _value = _successDefaultBoxed; - } - else - { - // Store the non-null success value itself. - // This does not incur boxing of the Success struct, but it does incur boxing of the TValue value if it is a value type. - _value = value.Value; - } + _kind = 1; + _value = value.Value; + _error = default!; } public Result(Failure value) { - if (_typeArgsMatch) - { - // both TValue and TError have intersecting types, so we must store the value as the Failure boxed. - _value = value; - } - else if (value.Error == null) - { - _value = _failureDefaultBoxed; - } - else - { - // Store the failure value itself. - // This does not incur boxing of the Failure struct, but it does incur boxing of the TError value if it is a value type. - _value = value.Error; - } + _kind = 2; + _value = default!; + _error = value.Error; } - public bool HasValue => _value != null; + public bool HasValue => _kind != 0; // has no value if uninitialized, otherwise has either success or failure. public bool TryGetValue(out Success value) { - if (_value is TValue val) - { - value = new Success(val); - return true; - } - else if (_value is Success successValue) + if (_kind == 1) { - value = successValue; + value = new Success(_value); return true; } else @@ -75,14 +46,9 @@ public bool TryGetValue(out Success value) public bool TryGetValue(out Failure value) { - if (_value is TError err) - { - value = new Failure(err); - return true; - } - else if (_value is Failure failureValue) + if (_kind == 2) { - value = failureValue; + value = new Failure(_error); return true; } else @@ -92,29 +58,40 @@ public bool TryGetValue(out Failure value) } } - public object? Value => _value switch + public object Value => _kind switch { - TValue val => new Success(val), - TError err => new Failure(err), - Success succ => succ, - Failure fail => fail, - _ => null + 1 => new Success(_value), + 2 => new Failure(_error), + _ => throw new System.InvalidOperationException("Result is uninitialized and has no value.") }; public static implicit operator Result(TValue value) => new Result(new Success(value)); public static implicit operator Result(TError error) => new Result(new Failure(error)); - - private readonly object _successDefaultBoxed = new Success(default!); - private readonly object _failureDefaultBoxed = new Failure(default!); } - +/// +/// Represents a successful result in a Result union. +/// public record struct Success(T Value); + +/// +/// Represents a failed result in a Result union. +/// public record struct Failure(T Error); +/// +/// Helper class for creating Result union instances. +/// public static class Result { + /// + /// Creates a instance with the specified value. + /// public static Success Success(TValue value) => new Success(value); + + /// + /// Creates a instance with the specified error. + /// public static Failure Failure(TError error) => new Failure(error); } \ No newline at end of file