Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 47 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

----
<br/>

## 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<TValue>` union type allows you to represent either `Some<TValue>` or `None` without boxing.

**Result**

## The UnionTypes.Toolkit.Generator Library
The `Result<TValue, TError>` union type allows you to represent either `Success<TValue>` or `Failure<TError>` 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<T1, T2>`, `Union<T1, T2, T3>`, 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<T1, T2>`, `FatUnion<T1, T2, T3>`, 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)
----
<br/>

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

Expand Down
4 changes: 2 additions & 2 deletions src/SourceGenerators.Package/ReadMe.Nuget.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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)
2 changes: 1 addition & 1 deletion src/UnionTypes.Tests/ResultTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public void Test_Default_HasNoValue()
{
Result<int, Exception> result = default;
Assert.IsFalse(result.HasValue);
Assert.IsTrue(result.Value is null);
Assert.ThrowsException<InvalidOperationException>(() => { var value = result.Value; });
}

[TestMethod]
Expand Down
51 changes: 18 additions & 33 deletions src/UnionTypes/Option.cs
Original file line number Diff line number Diff line change
@@ -1,44 +1,36 @@
namespace UnionTypes.Toolkit;

/// <summary>
/// A union that may contain either a <see cref="Some{T}"/> value or a <see cref="None"/> value.
/// It is similar to <see cref="System.Nullable{T}"/>, except the value can be also be a reference type and may use null as a valid value.
/// </summary>
[System.Runtime.CompilerServices.Union]
public struct Option<T>
: 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<T> 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<T> value)
{
if (_value is T val)
{
value = new Some<T>(val);
return true;
}
else if (_value is Some<T> someValue)
if (_hasValue)
{
value = someValue;
value = new Some<T>(_value);
return true;
}
else
Expand All @@ -50,9 +42,9 @@ public bool TryGetValue(out Some<T> value)

public bool TryGetValue(out None value)
{
if (_value == null)
if (!_hasValue)
{
value = Option.None;
value = new None();
return true;
}
else
Expand All @@ -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>(T value) => new Option<T>(new Some<T>(value));
public object Value =>
_hasValue ? new Some<T>(_value) : new None();

private readonly object _noneBoxed = new None();
private readonly object _someOfNoneBoxed = new Some<None>(new None());
public static implicit operator Option<T>(T value) => new Option<T>(new Some<T>(value));
}

/// <summary>
Expand Down
22 changes: 21 additions & 1 deletion src/UnionTypes/ReadMe.Nuget.md
Original file line number Diff line number Diff line change
@@ -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<TValue>` union type allows you to represent either `Some<TValue>` or `None`.

**Result**

The `Result<TValue, TError>` union type allows you to represent either `Success<TValue>` or `Failure<TError>`.

**Union**

A family of generic union types `Union<T1, T2>`, `Union<T1, T2, T3>`, 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<T1, T2>`, `FatUnion<T1, T2, T3>`, 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)
99 changes: 38 additions & 61 deletions src/UnionTypes/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,67 +3,38 @@

namespace UnionTypes.Toolkit;

/// <summary>
/// A union that may contain either a <see cref="Success{TValue}"/> value or a <see cref="Failure{TError}"/> value.
/// </summary>
[System.Runtime.CompilerServices.Union]
public struct Result<TValue, TError>
: 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<TValue> value)
{
if (_typeArgsMatch)
{
// both TValue and TError have intersecting types, so we must store the value as the Success<TValue> 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<TValue> 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<TError> value)
{
if (_typeArgsMatch)
{
// both TValue and TError have intersecting types, so we must store the value as the Failure<TError> boxed.
_value = value;
}
else if (value.Error == null)
{
_value = _failureDefaultBoxed;
}
else
{
// Store the failure value itself.
// This does not incur boxing of the Failure<TError> 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<TValue> value)
{
if (_value is TValue val)
{
value = new Success<TValue>(val);
return true;
}
else if (_value is Success<TValue> successValue)
if (_kind == 1)
{
value = successValue;
value = new Success<TValue>(_value);
return true;
}
else
Expand All @@ -75,14 +46,9 @@ public bool TryGetValue(out Success<TValue> value)

public bool TryGetValue(out Failure<TError> value)
{
if (_value is TError err)
{
value = new Failure<TError>(err);
return true;
}
else if (_value is Failure<TError> failureValue)
if (_kind == 2)
{
value = failureValue;
value = new Failure<TError>(_error);
return true;
}
else
Expand All @@ -92,29 +58,40 @@ public bool TryGetValue(out Failure<TError> value)
}
}

public object? Value => _value switch
public object Value => _kind switch
{
TValue val => new Success<TValue>(val),
TError err => new Failure<TError>(err),
Success<TValue> succ => succ,
Failure<TError> fail => fail,
_ => null
1 => new Success<TValue>(_value),
2 => new Failure<TError>(_error),
_ => throw new System.InvalidOperationException("Result is uninitialized and has no value.")
};

public static implicit operator Result<TValue, TError>(TValue value) => new Result<TValue, TError>(new Success<TValue>(value));
public static implicit operator Result<TValue, TError>(TError error) => new Result<TValue, TError>(new Failure<TError>(error));

private readonly object _successDefaultBoxed = new Success<TValue>(default!);
private readonly object _failureDefaultBoxed = new Failure<TError>(default!);
}


/// <summary>
/// Represents a successful result in a Result union.
/// </summary>
public record struct Success<T>(T Value);

/// <summary>
/// Represents a failed result in a Result union.
/// </summary>
public record struct Failure<T>(T Error);


/// <summary>
/// Helper class for creating Result union instances.
/// </summary>
public static class Result
{
/// <summary>
/// Creates a <see cref="Success{TValue}"/> instance with the specified value.
/// </summary>
public static Success<TValue> Success<TValue>(TValue value) => new Success<TValue>(value);

/// <summary>
/// Creates a <see cref="Failure{TError}"/> instance with the specified error.
/// </summary>
public static Failure<TError> Failure<TError>(TError error) => new Failure<TError>(error);
}
Loading