domn1995 / dunet Goto Github PK
View Code? Open in Web Editor NEWC# discriminated union source generator
License: MIT License
C# discriminated union source generator
License: MIT License
As a maintainer, I want a suite of integration tests, so that I can be confident that a changeset will behave correctly for end users.
#65 implemented synchronous matching with Action<T>
. Let's implement the asynchronous version as well, like what exists for Func<T>
.
As a developer, I want my union definitions to support non-primitive parameters, so that I can build more complex and useful unions.
I should be able to do something like this:
using System;
[Union]
interface IResult
{
void Created(Guid id);
void Failure(Exception error);
}
This currently fails to compile because the imports are not part of the generated code.
I am trying Dunet in a very simple piece of code.
I want to write unit tests where I say: If the returned type is correct, assert some properties, otherwise fail saying that the wrong options was returned.
My DU is:
Union]
internal partial record LogChange
{
partial record AddNew(Disconnection NewDisconnection);
partial record UpdateLastEndTime(Disconnection Last, DateTime EndTime);
partial record UpdateLastEndTimeAndAddNew(Disconnection Last, DateTime EndTime, Disconnection NewDisconnection);
}
So I would like to write the unit test as follows:
var result = BusinessLogic.DetermineLogChanges(e, null);
result.MatchAddNew(
addNew =>
{
Assert.Equal(expectedHardwarUnitId, addNew.NewDisconnection.HardwareUnitId);
Assert.Equal(expectedState, addNew.NewDisconnection.State);
},
// The else should be of the other type that was returned
@else => Assert.Fail($"Returned {@else} instead of {nameof(BusinessLogic.AddNew)}")
);
Unfortunately the MatchAddNew()
method only has an Action
without input parameter as else.
Is it possible to add an Action as an else, where T is the actual DU value returned?
Create a separate project with Attributes explicitly defined in code and referenced by the source generator.
This will help to avoid conflicts that generating the attribute current cause and the Attribute will be more easily discoverable.
An example in which the attributes are in a separate project from the source generator (and also not source generated in the project consuming the library)
https://github.com/CommunityToolkit/dotnet/tree/main/src/CommunityToolkit.Mvvm/ComponentModel/Attributes
Some times we just want to do something given a specific union value, rather than handling all cases explicitly. This is most useful on larger unions.
For example consider the following code:
[Union]
public partial record struct MathResult<T>
: where T : INumber<T>
{
public partial record struct Infinity;
public partial record struct Success(T Value);
public partial record struct NaN;
public partial record struct Undefined;
}
MathResult<double> result = // Some complex math calculation.
result
Match
method for each case?// A `MatchX()` is generated for each member `X` of the union. You must provide an else case.
var value = result.MatchSuccess(
// If `Infinity, NaN, or Undefined` return 0.
() => 0
);
// A `TryGetX` is generated for each member `X` of the union.
if (result.TryGetSuccess(out var success))
{
Console.WriteLine(success.Value);
}
I was thinking it might be quite nice to have an opt-in feature that makes you rely on factory methods to create the different types within a union. I was thinking this could be another attribute maybe [UnionWithFactory]
or something like [Union(factory: true)]
.
I took the code example from the samples and modified it to show how the factory methods could be used:
using Dunet.Choices;
using Dunet.Shapes;
var rectangle = Shape.Rectangle(10, 10);
var triangle = Shape.Triangle(10, 10);
var circle = Shape.Circle(10);
var getArea = (IShape shape) =>
shape switch
{
Rectangle rect => rect.Length * rect.Width,
Circle circle => 2.0 * Math.PI * circle.Radius,
Triangle triangle => 1.0 / 2.0 * triangle.Base * triangle.Height,
_ => 0d,
};
var rectangleArea = getArea(rectangle);
var triangleArea = getArea(triangle);
var circleArea = getArea(circle);
Console.WriteLine($"Rectangle area: {rectangleArea}");
Console.WriteLine($"Triangle area: {triangleArea}");
Console.WriteLine($"Circle area: {circleArea}");
var choice = GetChoice();
if (choice is Yes)
{
Console.WriteLine("YES!!!");
}
if (choice is No)
{
Console.WriteLine("NO!!!");
}
static IChoice GetChoice() =>
Console.ReadLine() switch
{
"yes" => Option.Yes(),
_ => Option.No()
};
What do you think?
I am attempting to replace my custom T4 based union generator with dunet. Currently, my implementation generates an Either
method which is functionally identical to the Match
method generated by dunet, and a Do
method, which is similar, but takes an Action
for each union case, and does not return a value. Dunet does not appear to currently have such a method. The generated code should be identical to the current Match
, except that it would return void
rather than TMatchOutput
, would take Action
instead of Func
delegates, and would not need to be generic on TMatchOutput
, as there would be no output.
As a developer, I want to use this library to generate monads, so that I don't have to write them manually.
I should be able to do something like this:
[Union]
public partial record Option<T>
{
partial record Some(T Value);
partial record None();
}
var option = new Option<string>.Some("foo");
var value = option.Match(
some => some.Value,
none => ""
);
Console.WriteLine(value); // "foo"
[Union]
public partial record Result<TFailure, TSuccess>
{
partial record Success(TSuccess Value);
partial record Failure(TFailure Error);
}
var result = new Result<Exception, string>.Failure(new Exception("Boom!"));
var message = result.Match(
success => success.Value,
failure => failure.Error.Message
);
Console.WriteLine(message); // "Boom!"
As a developer who is considering using Dunet, I want a collection of different real-world samples to help me determine if the library makes sense for my use case.
As a current user of Dunet, I want a collection of different real-world samples to help me better understand how/when to use the library.
Sample ideas:
I don't see the need in Match
function, when the is
pattern can work just as good (and even better, in terms of performance).
[Union]
partial record Result
{
partial record Ok();
partial record Error(string Id);
}
Result result = GetResult();
if(result is not Result.Error error)
Console.WriteLine("Success");
else
Console.WriteLine($"Error happened: {error.Id}");
Result result = GetResult();
Console.WriteLine(result switch {
Result.Ok => "Success",
Result.Error error => $"Error happened: {error.Id}",
_ => $"Unknown: {result}"
});
Hi,
Currently, the test CanReturnImplementationsOfGenericUnion
use default implementation of ToString
method for a double. This can cause errors when users have different culture-info settings.
Xunit.Sdk.XunitException
Expected actualArea to be "0.5", but "0,5" differs near ",5" (index 1).
at FluentAssertions.Execution.XUnit2TestFramework.Throw(String message)
at FluentAssertions.Execution.TestFrameworkProvider.Throw(String message)
at FluentAssertions.Execution.DefaultAssertionStrategy.HandleFailure(String message)
at FluentAssertions.Execution.AssertionScope.FailWith(Func`1 failReasonFunc)
at FluentAssertions.Execution.AssertionScope.FailWith(Func`1 failReasonFunc)
at FluentAssertions.Execution.AssertionScope.FailWith(String message, Object[] args)
at FluentAssertions.Primitives.StringEqualityValidator.ValidateAgainstMismatch()
at FluentAssertions.Primitives.StringValidator.Validate()
at FluentAssertions.Primitives.StringAssertions`1.Be(String expected, String because, Object[] becauseArgs)
at Dunet.Test.GenerateUnionRecord.GenericGenerationTests.CanReturnImplementationsOfGenericUnion(Int32 dividend, Int32 divisor, String expectedOutput) in C:\repos\external-repo\dunet\test\GenerateUnionRecord\GenericGenerationTests.cs:line 104
The suggested fix is to add InvariantCulture
parameter.
Note: Creating pull requests is blocked so I present the solution below:
We should support declaring unions as record structs
for consumers that want value semantics and reduced memory allocations. For example:
using Dunet;
[Union]
partial record struct Option<T>
{
partial record struct Some(T Value);
partial record struct None;
}
Mixing and matching structs and classes should not be allowed.
Design note: Since using structs we cannot use abstract
and inheritance to resolve the union type. We'll have to use a private discriminant like an enum
to support it and switch on it when matching.
Sometimes, we may want an easy way to get the value out of a union and don't care if the code fails at runtime.
Consider the following union:
[Union]
public partial record Option<T>
{
public partial record Some(T Value);
public partial record None;
}
Option<int> integer = 42;
We should be able to do something like this:
// If `integer` is `Option<int>.None` this would throw at runtime.
Option<int>.Some value = integer.UnwrapSome();
var value = integer.SomeOrThrow();
var value = integer.ForceSome();
var value = integer.ToSome();
As a developer, I want to use this source generator without having to include the source code for it, so that I can get started more quickly.
Hi, I've tried to use the basic usage on my machine with SDK .net 6.0.100, but I cannot compile due to the following errors:
This is my cproj and my Program.cs:
`
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dunet" Version="1.0.0" />
</ItemGroup>
`
`using Dunet;
var shape = new Shape.Rectangle(3, 4);
var area = shape.Match(
circle => 3.14 * circle.Radius * circle.Radius,
rectangle => rectangle.Length * rectangle.Width,
triangle => triangle.Base * triangle.Height / 2
);
Console.WriteLine(area);
[Union]
partial record Shape
{
partial record Circle(double Radius);
partial record Rectangle(double Length, double Width);
partial record Triangle(double Base, double Height);
}`
What I'm missing?
Consider the following union:
[Union]
public partial record Option<T>
{
public partial record Some(T Value);
public partial record None;
}
Currently, to instantiate the Some
variant, we must write the following:
var some = new Option<int>.Some(42);
Since the generic type parameter is bound to the union, we must define the type parameter every time since it cannot be inferred from the type passed to the union member. It would be nicer to be able to do the following:
// Inferred as `Option<int>.Some` since `42` is an integer.
var someInt = Option.Some(42);
// Inferred as `Option<string>.Some`.
var someStr = Option.Some("foo");
[Union]
public partial record Foo
{
partial record Bar(IEnumerable<string> Bars);
}
Dunet should not generate an implicit conversion from IEnumerable<string>
to Bar
.
I've been using the library in production code for a while now, and one use that would be useful is an async match method.
The use case here is when the matching logic is asynchronous due to a union being wrapped by a Task
.
Consider the following union:
[Union]
internal partial record HttpQueryResult<TBody>
{
public required HttpStatusCode StatusCode { get; init; }
public required Uri? RequestUri { get; init; }
public partial record Success(TBody Body);
public partial record Failure(string Error);
}
This code fails to compile because the generated implicit conversions do not initialize the required properties.
Some times we want to make a very basic union without naming its variants. We should be able to do something like:
public partial record struct Number : Union<int, double>;
This would enable us to do the following:
var integer = new Number.Int();
var @double = new Number.Double();
Should it support generics?
Should it require you to name its variants or infer from the type name?
[Union]
public partial record Result
{
partial record Ok(string Message);
partial record Error(Exception Exception)
{
// Can override specific methods on the generated types
public override string ToString() => Exception.ToString();
// ...or add new ones without resorting to extension methods
public bool CanIgnore => Exception is OperationCanceledException;
}
}
// Instantiate the Error case directly:
var result = new Result.Error(new Exception("hi :)"));
// Use the factory methods that return `Result`:
var otherResult = Result.NewError(new Exception("another way"));
result.Match(
(Result.Ok _) => 1,
(Result.Error e) => e.CanIgnore ? 1 : 0);
// Can use named arguments to make `match` more explicit:
result.Match(
error: e => e.CanIgnore ? 1 : 0,
ok: m => m.Message.Length);
Benefits
Result
as it has a private constructor.Result
class and the case classes directly rather than having to write them as extension methods.Match
method, as opposed to the extension method that you have currently.abstract partial record Result
{
// No other classes can derive from this class
private Result() {}
public static Result NewOk(string message) => new Ok(message);
public static Result NewError(Exception exception) => new Error(exception);
public abstract T Match<T>(Func<Ok, T> ok, Func<Error, T> error);
public sealed partial record Ok : Result
{
public override T Match<T>(Func<Ok, T> ok, Func<Error, T> error) =>
ok(this);
}
public sealed partial record Error : Result
{
public override T Match<T>(Func<Ok, T> ok, Func<Error, T> error) =>
error(this);
}
}
One issue with .Match
is that it allocated objects for case matching functions to capture the closure. C# has a mechanism to prevent this from happening with a static
keyword on lambda. However, for it to work the "closure" state has to be threaded manually.
Consider an example of such a match function called .FoldMatch
based on your Expression example implemented in the Expression
class. Functions StaticEvaluate
and EvaluateUniqueVar
show how such a utility could be used:
// Adapted from https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/discriminated-unions#using-discriminated-unions-for-tree-data-structures
using System.Collections.Immutable;
using Dunet;
using static Expression;
var environment = new Dictionary<string, int>()
{
["a"] = 1,
["b"] = 2,
["c"] = 3,
};
var expression = new Add(new Variable("a"), new Multiply(new Number(2), new Variable("b")));
// Evaluate a + 2 * b
var result = Evaluate(environment, expression);
Console.WriteLine(result); // 5
static int Evaluate(Dictionary<string, int> env, Expression exp) =>
exp.Match(
number => number.Value,
add => Evaluate(env, add.Left) + Evaluate(env, add.Right),
multiply => Evaluate(env, multiply.Left) * Evaluate(env, multiply.Right),
variable => env[variable.Value]
);
// Allows to evaluate expressions without allocating lambdas
static int StaticEvaluate(Dictionary<string, int> env, Expression exp) =>
exp.FoldMatch(
env,
static (_, number) => number.Value,
static (e, add) => StaticEvaluate(e, add.Left) + StaticEvaluate(e, add.Right),
static (e, multiply) => StaticEvaluate(e, multiply.Left) * StaticEvaluate(e, multiply.Right),
static (e, variable) => e[variable.Value]
);
// An example of how to thread state through a recursive function
// It simulates a (made up) use case where variables can be used only once
static (IImmutableDictionary<string, int> state, int result)
EvaluateUniqueVar(IImmutableDictionary<string, int> env, Expression exp) =>
exp.FoldMatch(
env,
static (e, number) => (e, number.Value),
static (e, add) =>
{
(e, var left) = EvaluateUniqueVar(e, add.Left);
(e, var right) = EvaluateUniqueVar(e, add.Right);
return (e, left + right);
},
static (e, multiply) =>
{
(e, var left) = EvaluateUniqueVar(e, multiply.Left);
(e, var right) = EvaluateUniqueVar(e, multiply.Right);
return (e, left * right);
},
static (e, variable) => (e.Remove(variable.Value), e[variable.Value])
);
[Union]
public partial record Expression
{
partial record Number(int Value);
partial record Add(Expression Left, Expression Right);
partial record Multiply(Expression Left, Expression Right);
partial record Variable(string Value);
public T FoldMatch<T, TState>(
TState state,
Func<TState, Number, T> number,
Func<TState, Add, T> add,
Func<TState, Multiply, T> multiply,
Func<TState, Variable, T> variable
) => this switch
{
Number x => number(state, x),
Add x => add(state, x),
Multiply x => multiply(state, x),
Variable x => variable(state, x),
_ => throw new InvalidOperationException()
};
}
As a dunet user, I want some unions to support assigning its members' inner values directly, so that my code is more terse and less noisy.
For example, we should be able to do this:
using Result = Result<System.Exception, string>;
// No `new Result.Err()` required here.
Result exception = new Exception("Boom!");
// No `new Result.Ok()` required here.
Result success = "Success!";
// Compiler error.
Result invalid = 1;
[Union]
public partial record Result<TError, TSuccess>
{
partial record Ok(TSuccess Value);
partial record Err(TError Error);
}
As a developer, I want a dedicated Match()
method, so that consumers of my union type are required to handle each case in order to get a value out of it.
Usage might look something like this:
[Union]
public interface IShape
{
IShape Circle(double radius);
IShape Rectangle(double length, double width);
IShape Triangle(double @base, double height);
}
var shape = new Rectangle(3, 4);
var area = shape.Match(
circle => 3.14 * circle.Radius * circle.Radius,
rectangle => rectangle.Length * rectangle.Width,
triangle => triangle.Base * triangle.Height / 2
);
Console.WriteLine(area); // "12"
In TypeScript, if all union cases have a field it may be accessed without matching. For example:
type Animal =
| { kind: 'dog', sound: 'woof', ageInDogYears: 12 }
| { kind: 'cat', sound: 'meow', ageInCatYears: 23 }
const t: Animal = // some value
t.kind // valid, since both have `kind`
t.sound // valid
t.ageInDogYears // error
I believe this library would benefit from this pattern when a property name and type are the same in all cases (TS allows the types to be different in which case it creates an anonymous union type for the return value; I guess it could also be implemented but I see it much less useful).
To implement it the base class needs to match on the type and then return this property, for example:
// Types.cs
[Union]
partial record Animal {
partial record Dog(string Sound) : Animal;
partial record Cat(string Sound) : Animal;
}
// Types.gen.cs
partial record Animal {
// possibly getter would be better
public string Sound() => this switch {
Dog(sound) => sound,
Cat(sound) => sound
}
}
Hi. First of all, I'd like to thank you for this excellent library! It is very useful in many contexts! That is great and impressive work! :)
It would be nice if the inner union classes worked. Below is the example that doesn't compile
using Dunet;
Console.WriteLine("Hello, World!");
public class Foo
{
[Union]
public partial record R
{
public partial record O1();
}
public R Bar()
{
return new R.O1();
}
}
The error says: Error CS0029 Cannot implicitly convert type 'Foo.R.O1' to 'Foo.R'
Although below code works just fine:
using Dunet;
Console.WriteLine("Hello, World!");
[Union]
public partial record R
{
public partial record O1();
}
public class Foo
{
public R Bar()
{
return new R.O1();
}
}
I would expect that both variants of code compile. The first version could be useful because the result type can be hidden inside the class. Is it possible to fix that?
Hello,
This does not work:
[Union]
public partial record Response
{
public partial record Ok;
public partial record Ok<T>;
}
It complains that the abstract members are not implemented.
Also this does not work:
[Union]
public partial record Response<T>
{
public partial record Ok;
public partial record Ok(T Data);
}
Or this:
[Union]
public partial record Response
{
public partial record Ok;
}
[Union]
public partial record Response<T>
{
public partial record Ok(T Data);
}
It would be useful and would help tooling like coverage reporters if the generated code was annotated with the GeneratedCodeAttribute.
At the moment, code generated for DU's is counted towards uncovered code:
Example here.
Coverlet finds only method 0.8% coverage in this workflow run, when it should find 50%, as the only uncovered method should be Main
Dunet doesn't currently support generating two unions with the same name in separate namespaces. This is due to the source generated output file being named after the union record's name here:
While most users probably won't run into this, I believe we should still support this use case to be more user friendly or prevent a silent failure that may be frustrating.
For example, I have a union defined like this:
[Dunet.Union]
public partial record BussinesLogicResult<T>
{
public partial record Ok(T Value);
public partial record EntityNotFound();
public partial record UnauthorizedAccess();
}
Well, I would like to override the ToString() method like this:
[Dunet.Union]
public partial record BussinesLogicResult<T>
{
public partial record Ok(T Value);
public partial record EntityNotFound();
public partial record UnauthorizedAccess();
public override string ToString()
{
return this.Match<string>(ok => ok.Value.ToString(),
entityNotFound => "Not found.",
unauthorizedAccess => "Unauthorized access.");
}
}
However, this has no effect as a result, because the records are implicitly override ToString() method. And it is necessary to override ToString() separately for each record.
My idea is that if the ToString() method is found in the base record, the public override string ToString() => base.ToString();
code will be generated in the uion members (children).
As a dunet user, when using switch expression with dunet-generated unions, I don't want to be warned about the switch expression not being exhaustive when it is provably so.
For example, the following should not emit a warning:
using static Option<int>;
Option<int> option = new Some(1);
var output = option switch
{
Some some => some.Value.ToString(),
None => "",
};
[Union]
public partial record Option<T>
{
partial record Some(T Value);
partial record None();
}
Hello,
I'm really sorry to ask here, but I would like to know how can I make it to work with asp (the serialization / deserialization part).
I created from scratch a repro:
https://github.com/titouancreach/repro-dunet
I've no problem in serializating my object to json, but the issue come when I try to deserialize it from json (see .http file)
I would be really grateful if you have any idea.
Thanks very muche
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.