domn1995/dunet

[Union] attribute on an `partial record` instead of `interface`?

Closed this issue · 3 comments

saul commented

Example usage

[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

  • With the generated code below, it is impossible to create another type that derives from Result as it has a private constructor.
  • It is possible to add your own method to the Result class and the case classes directly rather than having to write them as extension methods.
  • Untested hypothesis, but I imagine the JIT would generate better code with an abstract Match method, as opposed to the extension method that you have currently.

Source generator produces

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);
    }
}

I really like this idea! Seems more powerful and easier to generate sources for.

Implementation note
This should probably be implemented as an additional generator that gets triggered when an abstract record marked with the Union attribute changes. Don't think it would be a good idea to try to fit this functionality into DiscriminatedUnionGenerator.

If this method proves better, we can eventually deprecate the marked interface generator in favor of this new method.

With this implementation we should also be able to do:

using static Result;

// Instantiate the Error case directly:
var result = new Error(new Exception("hi :)"));

// Use the factory methods that return `Result`:
var otherResult = NewError(new Exception("another way"));

Closing as the core functionality is implemented by #21 and #25 and released in https://github.com/domn1995/dunet/releases/tag/v0.5.0

Will track factory method generation in #12.

Thanks so much for this awesome suggestion!