Coder Social home page Coder Social logo

mcintyre321 / oneof Goto Github PK

View Code? Open in Web Editor NEW
3.4K 49.0 156.0 860 KB

Easy to use F#-like ~discriminated~ unions for C# with exhaustive compile time matching

License: MIT License

C# 99.66% F# 0.24% PowerShell 0.10%
discriminated-unions c-sharp f-sharp dotnetcore

oneof's Introduction

OneOf NuGet GitHub

"Ah! It's like a compile time checked switch statement!" - Mike Giorgaras

Getting Started

install-package OneOf

This library provides F# style discriminated unions for C#, using a custom type OneOf<T0, ... Tn>. An instance of this type holds a single value, which is one of the types in its generic argument list.

I can't encourage you enough to give it a try! Due to exhaustive matching DUs provide an alternative to polymorphism when you want to have a method with guaranteed behaviour-per-type (i.e. adding an abstract method on a base type, and then implementing that method in each type). It's a really powerful tool, ask any f#/Scala dev! :)

PS If you like OneOf, you might want to check out ValueOf, for one-line Value Object Type definitions.

Use cases

As a method return value

The most frequent use case is as a return value, when you need to return different results from a method. Here's how you might use it in an MVC controller action:

public OneOf<User, InvalidName, NameTaken> CreateUser(string username)
{
    if (!IsValid(username)) return new InvalidName();
    var user = _repo.FindByUsername(username);
    if(user != null) return new NameTaken();
    var user = new User(username);
    _repo.Save(user);
    return user;
}

[HttpPost]
public IActionResult Register(string username)
{
    OneOf<User, InvalidName, NameTaken> createUserResult = CreateUser(username);
    return createUserResult.Match(
        user => new RedirectResult("/dashboard"),
        invalidName => {
            ModelState.AddModelError(nameof(username), $"Sorry, that is not a valid username.");
            return View("Register");
        },
        nameTaken => {
            ModelState.AddModelError(nameof(username), "Sorry, that name is already in use.");
            return View("Register");
        }
    );
}

As an 'Option' Type

It's simple to use OneOf as an Option type - just declare a OneOf<Something, None>. OneOf comes with a variety of useful Types in the OneOf.Types namespace, including Yes, No, Maybe, Unknown, True, False, All, Some, and None.

Benefits

  • True strongly typed method signature
    • No need to return a custom result base type e.g IActionResult, or even worse, a non-descriptive type (e.g. object)
    • The method signature accurately describes all the potential outcomes, making it easier for consumers to understand the code
    • Method consumer HAS to handle all cases (see 'Matching', below)
  • You can avoid using "Exceptions for control flow" antipattern by returning custom Typed error objects

As a method parameter value

You can use also use OneOf as a parameter type, allowing a caller to pass different types without requiring additional overloads. This might not seem that useful for a single parameter, but if you have multiple parameters, the number of overloads required increases rapidly.

public void SetBackground(OneOf<string, ColorName, Color> backgroundColor) { ... }

//The method above can be called with either a string, a ColorName enum value or a Color instance.

Matching

You use the TOut Match(Func<T0, TOut> f0, ... Func<Tn,TOut> fn) method to get a value out. Note how the number of handlers matches the number of generic arguments.

Advantages over switch or if or exception based control flow:

This has a major advantage over a switch statement, as it

  • requires every parameter to be handled

  • No fallback - if you add another generic parameter, you HAVE to update all the calling code to handle your changes.

    In brown-field code-bases this is incredibly useful, as the default handler is often a runtime throw NotImplementedException, or behaviour that wouldn't suit the new result type.

E.g.

OneOf<string, ColorName, Color> backgroundColor = ...;
Color c = backgroundColor.Match(
    str => CssHelper.GetColorFromString(str),
    name => new Color(name),
    col => col
);
_window.BackgroundColor = c;

There is also a .Switch method, for when you aren't returning a value:

OneOf<string, DateTime> dateValue = ...;
dateValue.Switch(
    str => AddEntry(DateTime.Parse(str), foo),
    int => AddEntry(int, foo)
);

TryPick𝑥 method

As an alternative to .Switch or .Match you can use the .TryPick𝑥 methods.

//TryPick𝑥 methods for OneOf<T0, T1, T2>
public bool TryPickT0(out T0 value, out OneOf<T1, T2> remainder) { ... }
public bool TryPickT1(out T1 value, out OneOf<T0, T2> remainder) { ... }
public bool TryPickT2(out T2 value, out OneOf<T0, T1> remainder) { ... }

The return value indicates if the OneOf contains a T𝑥 or not. If so, then value will be set to the inner value from the OneOf. If not, then the remainder will be a OneOf of the remaining generic types. You can use them like this:

IActionResult Get(string id)
{
    OneOf<Thing, NotFound, Error> thingOrNotFoundOrError = GetThingFromDb(string id);

    if (thingOrNotFoundOrError.TryPickT1(out NotFound notFound, out var thingOrError)) //thingOrError is a OneOf<Thing, Error>
      return StatusCode(404);

    if (thingOrError.TryPickT1(out var error, out var thing)) //note that thing is a Thing rather than a OneOf<Thing>
    {
      _logger.LogError(error.Message);
      return StatusCode(500);
    }

    return Ok(thing);
}

Reusable OneOf Types using OneOfBase

You can declare a OneOf as a type, either for reuse of the type, or to provide additional members, by inheriting from OneOfBase. The derived class will inherit the .Match, .Switch, and .TryPick𝑥 methods.

public class StringOrNumber : OneOfBase<string, int>
{
    StringOrNumber(OneOf<string, int> _) : base(_) { }

    // optionally, define implicit conversions
    // you could also make the constructor public
    public static implicit operator StringOrNumber(string _) => new StringOrNumber(_);
    public static implicit operator StringOrNumber(int _) => new StringOrNumber(_);

    public (bool isNumber, int number) TryGetNumber() =>
        Match(
            s => (int.TryParse(s, out var n), n),
            i => (true, i)
        );
}

StringOrNumber x = 5;
Console.WriteLine(x.TryGetNumber().number);
// prints 5

x = "5";
Console.WriteLine(x.TryGetNumber().number);
// prints 5

x = "abcd";
Console.WriteLine(x.TryGetNumber().isNumber);
// prints False

OneOfBase Source Generation

You can automatically generate OneOfBase hierarchies using GenerateOneOfAttribute and partial class that extends OneOfBase using a Source Generator (thanks to @romfir for the contribution :D). Install it via

Install-Package OneOf.SourceGenerator

and then define a stub like so:

[GenerateOneOf]
public partial class StringOrNumber : OneOfBase<string, int> { }

During compilation the source generator will produce a class implementing the OneOfBase boiler plate code for you. e.g.

public partial class StringOrNumber
{
	public StringOrNumber(OneOf.OneOf<System.String, System.Int32> _) : base(_) { }

	public static implicit operator StringOrNumber(System.String _) => new StringOrNumber(_);
	public static explicit operator System.String(StringOrNumber _) => _.AsT0;

	public static implicit operator StringOrNumber(System.Int32 _) => new StringOrNumber(_);
	public static explicit operator System.Int32(StringOrNumber _) => _.AsT1;
}

oneof's People

Contributors

bartecargo avatar davidhunter22 avatar dependabot[bot] avatar dnperfors avatar gps035 avatar haskellcamargo avatar ijsgaus avatar jagrem avatar jamesgecargo avatar jtone123 avatar khellang avatar luhis avatar mcintyre321 avatar ndrwrbgs avatar nicomoya123 avatar nxsoftware avatar romfir avatar rtkelly13 avatar ruan92 avatar rutgersc avatar slang25 avatar teo-tsirpanis avatar thomhurst avatar tkilfree avatar xanderstoffels avatar zspitz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

oneof's Issues

Consider implicit conversions to Tx

E.g.

public static implicit operator T0(OneOf<T0, T1> o) => o.AsT0;

I tested and the primary concern of OneOf<int, int> is mitigated since the language warns on such an attempt:

var test = new OneOf<int, int>();
int value = test; // Fails to compile due to ambiguous invocation, user would have to use `AsT0`/`AsT1` directly

Generic As<T>

Have you considered a generic As, something like the following

        public T As<T>( )
        {
            if( typeof( T ) == typeof( T0 ) ) return (T)(object)AsT0;
            if( typeof( T ) == typeof( T1 ) ) return (T)(object)AsT1;
            if( typeof( T ) == typeof( T2 ) ) return (T)(object)AsT2;

            throw new InvalidOperationException( $"Cannot return as {typeof( T ).Name} does not contain that type" );
        } 

So you could write things like

    OneOf<int,double,string> x = ...

    var y = x.As<string>( );

This would avoid having to remember index values and having to change them if you say add an extra type. Note this is similar to the C++ std::variant get function which is very useful.

I did try implementing the above and it did seem to work. I could make a pull request but wanted to check if it would be accepted first.

Considering NamedOneOf replacement for OneOfBase

The OneOfBase implementation has some flaws (#28 and #45) where inheritance causes some stack overflow problems. Eek.

I'm considering replacing it with a completely different OneOf<T0, .., TN>.Named<TName> type. Inheritance was only really needed to save on typing implicit operators, but I think it's caused more problems than it's worth.

Thoughts welcome.
e.g.

public sealed class PaymentResult : 
    OneOf<PaymentResult.Success, PaymentResult.Declined>
    .Named<PaymentResult>
{
	private PaymentResult(){}
	
	public class Success  { }
	public static implicit operator PaymentResult(Success x) => PaymentResult.From(x);

	public class Declined { }
	public static implicit operator PaymentResult(Declined x) => PaymentResult.From(x);
}


public partial struct OneOf<T0, T1> {

    public class Named<TName> : IOneOf where TName : Named<TName>
    {
	...
	public static TName From(T0 t)
	{
		var instance = (TName)System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(TName)); ;
		instance._index = 0;
		instance._value0 = t;
		return instance;
	}

	public static TName From(T1 t)
	{
		var instance = (TName)System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(TName)); ;
		instance._index = 1;
		instance._value1 = t;
		return instance;
	}

	...
    }
}

This would come with a move to netstandard2.0 in order to use FormatterServices.GetUninitializedObject, and a major version bump. I'd probably remove the old OneOfBase

Maybe been a bit too 'clever' nesting the Named type in the OneOf, but it's quite neato...

Use StructLayout for reduced memory usage

When OneOf is used with many value-type arguments the value itself can get quite large.

For example, this Integer type below is 34 bytes big.

using Integer = OneOf<SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64>; 
// 1 + 1 + 2 + 2 + 4 + 4 + 8 + 8 + 4 for _index
sizeof( Integer ) == 34

This can be improved by using struct packing.

For example:

[StructLayout(LayoutKind.Explicit)]
    public struct OneOf<T0, T1, T2, T3, T4, T5, T6, T7> : IOneOf
    {
		[FieldOffset( offset: 0 )]
		readonly Byte _index;

		[FieldOffset( offset: 1 )]
        readonly T0 _value0;
		[FieldOffset( offset: 1 )]
        readonly T1 _value1;
		[FieldOffset( offset: 1 )]
        readonly T2 _value2;
		[FieldOffset( offset: 1 )]
        readonly T3 _value3;
		[FieldOffset( offset: 1 )]
        readonly T4 _value4;
		[FieldOffset( offset: 1 )]
        readonly T5 _value5;
		[FieldOffset( offset: 1 )]
        readonly T6 _value6;
		[FieldOffset( offset: 1 )]
        readonly T7 _value7;

And now, sizeof(Integer) == 9.

Using a Byte for _index helps - but may harm performance owing to not being native-word-aligned anymore.

Thoughts?

async with Match / Switch

Perhaps I am missing something obvious, but there appears to be no way of doing an async version of Match / Switch.

Is there anyway this could be supported?

Default says that it's T0

Problem: default(OneOf<T...>) will say that it is T0, and return default(T0) for .Value
Suggestion: Modify _index to be 1-based-index instead of 0-based, so that default(OneOf) can be distinguished from OneOf.FromT0(default)

var value = default(OneOf<int, string>);
Assert.IsFalse(value.IsT0); // True today
Assert.IsFalse(value.IsT1);
Assert.AreNotEqual(default(int), value.Value); // True today

Consider Including Option<T> and Result<T>Types

Option and Result types are usually implemented as a discriminated union under the hood and allow for clearer interfaces than generic discriminated unions. I believe it would be wise to include these types in this project or start a project for providing these types based on OneOf.

For reference this is the implementation I've come up with:

namespace OneOfExt
{
    public class InvalidOptionAccessException : Exception
    {
        public InvalidOptionAccessException() : base() { }
        public InvalidOptionAccessException(string message) : base(message) { }
    }

    public class FailureException : Exception
    {
        public FailureException(string message) : base(message) { }
    }

    public class NonFailureException : Exception { };

    public static class Option
    {
        public struct NoneT { };
        public static NoneT None => new NoneT { };
    }

    public struct Option<T> : IEquatable<Option<T>>
    {
        private readonly OneOf<T, Option.NoneT> oneOf;
        public static Option.NoneT None => new Option.NoneT { };
        public Option(Option.NoneT none) { oneOf = Option.None; }
        public Option(T value) { oneOf = value; }
        public Option(OneOf<T, Option.NoneT> oneOf) { this.oneOf = oneOf; }
        public static implicit operator Option<T>(Option.NoneT _) => new Option<T>();
        public static implicit operator Option<T>(T value) => new Option<T>(value);
        public static implicit operator Option<T>(OneOf<T, Option.NoneT> oneOf) => new Option<T>(oneOf);
        public T SomeOr(T default_) => oneOf.Match(value => value, _ => default_);
        public T SomeOrThrow(Exception exception) => oneOf.Match(value => value, _ => throw exception);
        public T Some => SomeOrThrow(new InvalidOptionAccessException());
        public void Do(Action<T> action) => DoOrThrow(action, new InvalidOptionAccessException());
        public void DoOrThrow(Action<T> action, Exception ex) => oneOf.Switch(value => action(value), _ => throw ex);
        public bool TryDo(Action<T> action) => oneOf.Match(value => { action(value); return true; }, _ => false);
        public bool IsSome => oneOf.Match(value => true, _ => false);
        public bool IsNone => !IsSome;
        public bool Equals(Option<T> other) => oneOf.Equals(other.oneOf);
        public override bool Equals(object obj) => obj is Option<T> opt ? Equals(opt) : false;
        public static bool operator ==(Option<T> a, Option<T> b) => a.Equals(b);
        public static bool operator !=(Option<T> a, Option<T> b) => !(a == b);
        public static bool operator ==(Option<T> a, T b) => a.oneOf.Match(value => value.Equals(b), _ => false);
        public static bool operator !=(Option<T> a, T b) => !(a == b);
        public static bool operator ==(Option<T> a, Option.NoneT b) => a.oneOf.Match(value => false, _ => true);
        public static bool operator !=(Option<T> a, Option.NoneT b) => !(a == b);
        public override int GetHashCode() => oneOf.GetHashCode();
        public override string ToString() => oneOf.Match(value => value.ToString(), _ => "None");
        public bool TryPickValue(out T value) => oneOf.TryPickT0(out value, out _);
        public OneOf<TResult, Option.NoneT> MapValue<TResult>(Func<T, TResult> mapFunc) => oneOf.MapT0(mapFunc);
    }

    public struct Failure
    {
        private readonly OneOf<Exception, string> oneOf;
        public Failure(Exception ex) { oneOf = ex; }
        public Failure(string msg) { oneOf = msg; }
        public Failure(OneOf<Exception, string> oneOf) { this.oneOf = oneOf; }
        public static implicit operator Failure(Exception ex) => new Failure(ex);
        public static implicit operator Failure(string msg) => new Failure(msg);
        public static implicit operator Failure(OneOf<Exception, string> oneOf) => new Failure(oneOf);
        public static implicit operator Exception(Failure f) => f.ToException();
        public TResult Match<TResult>(Func<Exception, TResult> fEx, Func<string, TResult> fStr) => oneOf.Match(ex => fEx(ex), str => fStr(str));
        public void Switch(Action<Exception> fEx, Action<string> fStr) => oneOf.Switch(ex => fEx(ex), str => fStr(str));
        public Exception ToException() => oneOf.Match(ex => ex, msg => new FailureException(msg));
        public void ThrowException() => throw ToException();
        public override string ToString() => oneOf.Match(ex => ex.ToString(), msg => msg);
    }

    public struct Result
    {
        public struct SuccessT { };
        public static SuccessT Success => new SuccessT { };

        private readonly OneOf<SuccessT, Failure> oneOf;
        public Result(SuccessT success) { oneOf = success; }
        public Result(Failure failure) { oneOf = failure; }
        public Result(OneOf<SuccessT, Failure> oneOf) { this.oneOf = oneOf; }
        public static implicit operator Result(Failure failure) => new Result(failure);
        public static implicit operator Result(SuccessT success) => new Result(success);
        public static implicit operator Result(OneOf<SuccessT, Failure> oneOf) => new Result(oneOf);
        public void ThrowIfFailure() => oneOf.Switch(s => { }, f => f.ThrowException());
        public Failure Failure => oneOf.Match(s => throw new NonFailureException(), f => f);
        public bool IsSuccess => !IsFailure;
        public bool IsFailure => oneOf.Match(f => true, s => false);
        public bool TryPickFailure(out Failure failure) => oneOf.TryPickT1(out failure, out _);
    }

    public struct Result<T> 
    {
        private readonly OneOf<T, Failure> oneOf;
        public Result(Failure failure) { oneOf = failure; }
        public Result(T value) { oneOf = value; }
        public Result(OneOf<T, Failure> oneOf) { this.oneOf = oneOf; }
        public static implicit operator Result<T>(Failure failure) => new Result<T>(failure);
        public static implicit operator Result<T>(T value) => new Result<T>(value);
        public static implicit operator Result<T>(OneOf<T, Failure> oneOf) => new Result<T>(oneOf);
        public TResult Match<TResult>(Func<T, TResult> fVal, Func<Failure, TResult> fFail) => oneOf.Match(value => fVal(value), failure => fFail(failure));
        public void Switch(Action<T> fVal, Action<Failure> fFail) => oneOf.Switch(value => fVal(value), failure => fFail(failure));
        public void Do(Action<T> action) => Switch(action, failure => failure.ThrowException());
        public bool TryDo(Action<T> action) => Match(value => { action(value); return true; }, failure => false);
        public void ThrowIfFailure() => oneOf.Switch(value => { }, failure => failure.ThrowException());
        public T SuccessElse(T default_) => oneOf.Match(value => value, failure => default_);
        public T Success => oneOf.Match(value => value, failure => throw failure.ToException());
        public Failure Failure => oneOf.Match(value => throw new NonFailureException(), failure => failure);
        public bool IsSuccess => oneOf.Match(value => true, failure => false);
        public bool IsFailure => !IsSuccess;
        public bool TryPickValue(out T value, out Failure failure) => oneOf.TryPickT0(out value, out failure);
        public bool TryPickFailure(out Failure failure, out T value) => oneOf.TryPickT1(out failure, out value);
        public Result<TResult> Map<TResult>(Func<T, TResult> mapFunc) => oneOf.MapT0(mapFunc);
    }
}

.NET Framework 3.5

Hi, Can the library support .NET Framework 3.5. I tried a local build adding net35 to and it all seemed to build. Sadly 3.5 is still supported by Microsoft and for various reasons we have to build against it as a minimum version.

Infinite recursion in OneOfBase.GetHashCode() and Equals()

If OneOfBase is used with the case classes being subclasses of the OneOfBase subclass, as in the readme:

    public abstract class PaymentResult : OneOfBase<PaymentResult.Success, PaymentResult.Declined, PaymentStatus.Failed>
    {
        public class Success : PaymentResult { }  
        public class Declined : PaymentResult { }
        public class Failed  : PaymentResult { public string Reason { get; set; } }
    }

and GetHashCode is not overridden in the case classes, OneOfBase<...>.GetHashCode will just call itself, resulting in a stack overflow. (If it is overriden, it works, but the base impl becomes pointless.). Same problem with Equals.

Not sure offhand what would be the best solution, especially without breaking the use case where the case types are not subclasses of the OneOfBase, and are possibly value types. And without making things harder for users who don't care about equality comparison (e.g. by adding a type constraint like IEquatableCaseClass).

Missing release notes?

I noticed the project got a major version bump recently, however I could not find any information about this new release, what new features there are, bug fixes, etc. I noticed the "Releases" section here on github is empty, so you haven't been using it.

Are there any plans to expose release notes to consumers in a cleaner, easy-to-find way?

Option to build with reduced maximum option count

As it is, the library size is ridiculous, it's over 10 MB, which makes it very prohibitive to use in some case. Most people won't need variants with 30 options. The System.Action generic delegate only has variants with up to 16 parameters, for example.

Have `Switch` and `Match` use `switch` internally for performance?

I noticed that OneOf<T...>'s Switch and Match methods internally use sequential tests and branching which can be optimized to use switch. The test for fN != null can be included with minimal extra syntax.

From this:

        public TResult Match<TResult>(Func<T0, TResult> f0, Func<T1, TResult> f1, Func<T2, TResult> f2, Func<T3, TResult> f3, Func<T4, TResult> f4, Func<T5, TResult> f5, Func<T6, TResult> f6, Func<T7, TResult> f7, Func<T8, TResult> f8)
        {
            if (_index == 0 && f0 != null)
            {
                return f0(_value0);
            }
            if (_index == 1 && f1 != null)
            {
                return f1(_value1);
            }
            if (_index == 2 && f2 != null)
            {
                return f2(_value2);
            }
            if (_index == 3 && f3 != null)
            {
                return f3(_value3);
            }
            if (_index == 4 && f4 != null)
            {
                return f4(_value4);
            }
            throw new InvalidOperationException();
        }

to this:

public TResult Match<TResult>(Func<T0, TResult> f0, Func<T1, TResult> f1, Func<T2, TResult> f2, Func<T3, TResult> f3, Func<T4, TResult> f4, Func<T5, TResult> f5, Func<T6, TResult> f6, Func<T7, TResult> f7, Func<T8, TResult> f8)
        {
            switch(_index)
            {
            case 0:
                if( f0 == null ) goto default;
                return f0.Invoke( _value0 );

            case 1:
                if( f1 == null ) goto default;
                return f1.Invoke( _value1 );

            case 2:
                if( f2 == null ) goto default;
                return f2.Invoke( _value2 );

            case 3:
                if( f3 == null ) goto default;
                return f3.Invoke( _value3 );

            case 4:
                if( f4 == null ) goto default;
                return f4.Invoke( _value4 );

            default:
                throw new InvalidOperationException();
            }
}

OneOfBase cannot be used as base class

If i inherit from OneOfBase:

public class Await
{
   private Await() {}
   public static Await Default { get; } = new Await();
}

public class Sygnal<T> : OneOfBase<T, Exception, None, Await>
{
         public static Sygnal<T> Await { get; } = Await.Default; // this is impossible
}

I need protected constructor or other option.

Mocking OneOf for tests

I have just started using this library, works great in the implementation/in runtime, but cannot figure out how to return a oneof from a mocked test setup. How can one "new" a oneof? Is there an Moq Extension for oneof?

ToString() causing a StackOverflowException

Given the following

public abstract class PaymentResult : OneOfBase<PaymentResult.Success, PaymentResult.Declined, PaymentResult.Failed>
{
	public class Success : PaymentResult { }
	public class Declined : PaymentResult { }
	public class Failed : PaymentResult { public string Reason { get; set; } }
}

Calling ToString() results in a StackOverflowException

// Causes a StackOverflowException
new PaymentResult.Declined().ToString();

Unfortunately this is completely breaking debugging in any IDE that attempts to evaluate a variable of type OneOfBase by displaying it's ToString() representation.

Consolidate all types into a single namespace

I notice that the utility types like All, Error, None, etc are in the OneOf.Types namespace.

Is there a reason they're in a child namespace? It seems odd to only have IOneOf, OneOf<T...> and OneOfBase<T...> in OneOf but have other types you're bound to use a child namespace.

As a consumer of OneOf, it's a hassle to import two namespaces every time (y'know... scroll back to the top, type out u.s.i.n.g.<space>O..n.<ctrl+space><down><down><enter>). I know <Ctrl> + <Dot> speeds things up in VS2017 but it isn't perfect when in ambiguous contexts.

Also, I know it sounds crazy - but might you even consider making an alternative NuGet package where the types are pre-inserted into the System namespace? (Ordinarily I feel third-party packages should never be in the System namespace, but I'd make an exception for this package because it's just so danged useful).

Question: Converting from subtype value to class derived from OneOfBase

Given the following definition:

public class MetaValue : OneOfBase<
    bool,
    string
> { }

How do I get from either a bool or string to MetaValue?

For OneOf, this is pretty straightforward:

OneOf<bool, string> x = true;
x = "abcd";

But trying something similar with OneOfBase:

MetaValue m = true;

doesn't compile with:

CS0266: Cannot implicitly convert type 'bool' to 'PandocFilters.Types.MetaValue'. An explicit conversion exists (are you missing a cast?)

And the explicit cast:

var m = (MetaValue)true;

fails at runtime with System.InvalidCastException.

How am I supposed to create an instance of MetaValue?

Add ToString formatter to OneOf

Right now, formatting an instance of OneOf results in a string like this:
OneOf.OneOf`2[Some.Custom.Type]. Do you have any objections to me submitting a PR with a custom formatter that outputs a more useful representation of the underlying value?

If you're OK with that, what format would you like the string to take?

Match Syntax Exception

Hi All,

I'm just exploring OneOf for my Integration Test.

I'm getting syntax error, while doing below. Can you please help me to resolve this?

image

OneOf<User, InvalidName, NameTaken> response = await CreateUser("username");

 response.Match(
                user =>
                {
                    Console.Write("I'm User");
                },
                name =>
                {
                    Console.Write("I'm name ");
                },
                taken =>
                {
                    Console.Write("I'm taken ");
                });
public async Task<OneOf<User, InvalidName, NameTaken>> CreateUser(string username)
        {
            if (username is null)
                return new InvalidName();
            if (username == "Test User")
                return new NameTaken();
            var user = new User();
            return user;
        }

Proposal: Save space by removing delegates from TryPick and Map autogenerated implementations

The autogenerated code for the TryPickX method currently looks like this:

public OneOf<T0, T1, TResult, T3> MapT2<TResult>(Func<T2, TResult> mapFunc)
{
    if(mapFunc == null)
    {
        throw new ArgumentNullException(nameof(mapFunc));
    }
    return Match<OneOf<T0, T1, TResult, T3>>(
        input0 => input0,
        input1 => input1,
        input2 => mapFunc(input2),
        input3 => input3
    );
}

Each delegate requires the compiler to generate its own class. Similarly, for Map:

public OneOf<T0, T1, TResult, T3> MapT2<TResult>(Func<T2, TResult> mapFunc)
{
    if(mapFunc == null)
    {
        throw new ArgumentNullException(nameof(mapFunc));
    }
    return Match<OneOf<T0, T1, TResult, T3>>(
        input0 => input0,
        input1 => input1,
        input2 => mapFunc(input2),
        input3 => input3
    );
}

Using autogenerated implementations that don't have delegates, e.g. for TryPick:

public bool TryPickT1(out T1 value, out OneOf<T0, T2, T3, T4> remainder)
{
    value = IsT1 ? AsT1 : default;
    switch (_index)
    {
        case 0: remainder = OneOf<T0, T2, T3, T4>.FromT0(AsT0); break;
        case 1: remainder = default; break;
        case 2: remainder = OneOf<T0, T2, T3, T4>.FromT1(AsT2); break;
        case 3: remainder = OneOf<T0, T2, T3, T4>.FromT2(AsT3); break;
        case 4: remainder = OneOf<T0, T2, T3, T4>.FromT3(AsT4); break;
        default: throw new InvalidOperationException();
    }
    return IsT1;
}

and for Map

public OneOf<T0, TResult, T2, T3, T4> MapT1<TResult>(Func<T1, TResult> mapFunc)
{
    if(mapFunc == null)
    {
        throw new ArgumentNullException(nameof(mapFunc));
    }
    switch (_index)
    {
        case 0: return OneOf<T0, TResult, T2, T3, T4>.FromT0(AsT0);
        case 1: return OneOf<T0, TResult, T2, T3, T4>.FromT1(mapFunc(AsT1));
        case 2: return OneOf<T0, TResult, T2, T3, T4>.FromT2(AsT2);
        case 3: return OneOf<T0, TResult, T2, T3, T4>.FromT3(AsT3);
        case 4: return OneOf<T0, TResult, T2, T3, T4>.FromT4(AsT4);
        default: throw new InvalidOperationException();
    }
}

reduces the file size, as follows:

File Current Reduced
OneOf.dll 233K 106K
OneOf.Extended.dll 9.9M 1.9M

I've never had much need to consider this before, but I'm wondering if there are other ways to reduce the size of the IL? Maybe use later language features such as switch expressions? Or perhaps replace internal calls to FromX with a generic FromIndex method with a generic parameter?

How much of a reduction would be needed to move OneOf.Extended back into the original dll?

Considering Option helper

Hello watchers! I'm considering adding an Option helper, so that

OneOf<Foo, None> Get(string id){
        var foo = _repository.Query<Foo>(id).SingleOrDefault();
        if (foo == null) return new None();
	return foo;
}

can be written as

OneOf<Foo, None> Get(string id){
	return Option.Of(_repository.Query<Foo>(id).SingleOrDefault());
}

Implementation would look something like:

public static class Option
{
	public static OneOf<T, None> Of<T>(T? item) where T : struct  => (item.HasValue) ? (OneOf<T, None>) item : new None();

	public static OneOf<T, None> Of<T>(T item) where T: class => (item == null) ? (OneOf<T, None>) new None() : item;
}

Any feedback welcome.

Deconstruct support

Would it be possible to add Deconstruct support to this like this?

The use case I have is this:

public OneOf<Result1, Error1, Error2> Fn1(...) { ... }
public OneOf<Result2, Error1, Error2, Error3> Fn2(...)
{
    var result = Fn1(...);
    // If result is Error1 just return it
    // If result is Error2 just return it
    // If result is Result1 then:
    //     - If there's some other condition return Error3
    //     - otherwise return the transformation of Result1 to Result2
}

The way I thought about it is this:

public OneOf<Result1, Error1, Error2> Fn1(...) { ... }
public OneOf<Result2, Error1, Error2, Error3> Fn2(...)
{
    var (result, error1, error2) = Fn1(...);
    if (error1 != null) return error 1;
    if (error2 != null) return error 2;
    
    if (Condition(result)) return new Error3();
    return Transform(result);
}

Problems with implicit cast

I've run into scenario involving an implicit cast that I expected to work, but which produces a compiler error. Here's a test case:

[Test]
public void ImplicitCasts()
{
    OneOf<string, int> a = 123;
    OneOf<string, int> b = "abc";
    OneOf<IEnumerable<string>, int> c = 123;
    OneOf<IEnumerable<string>, int> d = new string[0];
    OneOf<IEnumerable<string>, int> e = (IEnumerable<string>) new string[0];
}

The first four statements are fine, but the fifth statement causes the following compiler error:
error CS0029: Cannot implicitly convert type 'System.Collections.Generic.IEnumerable' to 'OneOf.OneOf<System.Collections.Generic.IEnumerable, int>'.

Any thoughts? Do you think the cast should work? It's particularly puzzling me why the fourth statement is fine.

Auto-generation code improvements

  1. Move the code from a .linq file to an additional project under the solution. Will allow using IDE features and debugging without having to purchase a LINQPad license.
  2. Set LangVersion in the OneOf.csproj and OneOf.Extended.csproj to 9.0, and emit newer C# language syntax such as switch expressions. Note that for much of the syntax, a higher .NET TFM is not required.
  3. Remove for and multiple AppendLine calls in an iteration; prefer a single AppendLine with joined strings.

Add a .SwitchAsync method which behaves as .Match returning a Task

I've seen too many times developers using .Switch instead of .Match when calling async void code inside the lambdas, because the default behaviour when you want to perform a void operation is to .Switch rather than .Match, resulting in

OneOf<T1, T2> res = GetResult();

res.Switch(async t1 => await ProcessT1(t1), async t2 => await ProcessT2(t2));

which isn't awaited correctly (resulting in exceptions being lost)

A .SwitchAsync method which accepts a Func<T, Task> for each parameter would make it really clear than an async option can be used (even gives an IDE hint), and the behaviour would be the same as .Match(Func<T, Task>)

Add description to README of how to use OneOf and OneOfBase types with `switch` statements and expressions

Now that Index is exposed on all OneOf and OneOfBase, via the IOneOf.Index property, it should be trivial to use C# control flow syntax as an alternative to Match, Switch, and TryPick𝑥:

public class StringOrNumber : OneOfBase<string, int> {
    StringOrNumber(OneOf<string, int> _) : base(_) { }
    public static implicit operator StringOrNumber(string _) => new StringOrNumber(_);
    public static implicit operator StringOrNumber(int _) => new StringOrNumber(_);
}

StringOrNumber x = 5;
int? result = x.Index switch {
    0 => int.TryParse(x.AsT0, out var n) ? n : null,
    1 => x.AsT1
};

Should this be documented in the README?

It might be preferable to do this in certain case where creating all the necessary delegate objects could be a performance or memory issue.

ASP.NET Controller InvalidOperationException

Couldnt find anyones contact details to email so putting this on here.
Can someone explain why I get the following error in my ASP.NET 5 MVC WebAPI project:

InvalidOperationException: Cannot return as T1 as result is T0

I have an API Controller Get Method defined as:
public OneOf<String,Int32> GetTest()
{
if (_Message != null)
return "NotNull";
else
return 0;
}
Why do i get the exception? I know its to do with Json Serialization but not sure how to handle this?
Any help appreciated.

Ambiguous reference between OneOf namespace and types

When trying to use the OneOf<> generic types they are ambiguous with the OneOf namespace which leads to the need for fully specifying the type as OneOf.OneOf<>.

This isn't easily resolvable with a using directive since the OneOf<> types are generic.

It's generally a bad idea to have a type within a namespace have the same name as the namespace. Consider changing the namespace? Or is there an alternative that I might be unaware of to fix the situation?

Conversion from OneOf<A, B> to OneOf<B, A> and similar

The notion of a discriminated union usually doesn't make a difference between union X = A | B and union X = B | A. Therefore it seems to me that the followings should be implicitly convertible:

  • A -> OneOf<A, B>
  • OneOf<A, B> -> OneOf<B, A>

Moreover this would be beneficial:

  • OneOf<A, B> -> OneOf<A, B, C>

Is there any way in which this could be implemented other than enumerate all the possible combinations? Or how could this be implemented? Should this be implemented?

Using OneOf without adding dependency on Newtonsoft.Json

Some people might appreciate having a OneOf package without pulling in Newtonsoft.Json if they aren't using it.

Is it possible to allow IOneOf to interop with Newtonsoft.Json through BCL mechanisms rather than through a JsonConverter? For example, implicit operator or TypeConverter?

Consider OneOf<T>.Is(Type type)

It could make code such as

var test = SomeMethod();
if (test.IsT0) {
}

potentially more readable?

var test = SomeMethod();
if (test.Is(typeof(int))) {
}

Help: returning a different OneOf from within a Match

Hi Harry

I'm new to OneOf and am having a play with it at work, mainly using it as an Option type via OneOf<T, None>.

I have a couple of questions - 1. how do we avoid massive nesting within Matches when you need to do something and then something else and then something else and then something else?

  1. How do I return a different OneOf<> from within a Match? My use case is this:
private async Task<OneOf<string, None>> GetContent(string requestUri, CorrelationId correlationId)
{
    var response = await client.GetAsync(requestUri); // client is an HttpClient

    var content = response.Content == null ? string.Empty : await response.Content.ReadAsStringAsync();

    if (response.IsSuccessStatusCode)
    {
        return content;
    }

    return new None();
}

public async Task<OneOf<Address, None>> AddressSearch(string term, CorrelationId correlationId)
{
    var requestUri = AddressSearchUri
        .AddQueryParam("client_id", clientId)
        .AddQueryParam("term", term);

    var content = await GetContent(requestUri, correlationId);

    return content.Match(
        str => 
        {
            var addressSearchResponse = str.FromJson<AddressSearchResponse>();

            if (addressSearchResponse?.RetrieveLocationResponse?.BusinessResult?.Success != "Y")
            {
                return new None();
            }

            if (addressSearchResponse?.RetrieveLocationResponse?.TypeAheadSearch == null)
            {
                return new None();
            }

            return new Address( // this line fails to compile - "Cannot implicitly convert type Address to OneOf.Types.None"
                addressSearchResponse.RetrieveLocationResponse.TypeAheadSearch[0].Elid,
                addressSearchResponse.RetrieveLocationResponse.TypeAheadSearch[0].Label);
        },
        none => new None()
    );
}

How I convert between OneOfs holding different numbers of types?

I'm trying to create a OneOf<X, Y> from a OneOf<A, B, C> where I can map A => X, B => Y and C => Y. What is the best way to achieve this?

Here are things I've tried that don't work.

// Fails to compile
// Cannot implicitly convert type 'X' to 'OneOf.OneOf<X, Y>' csharp(CS0029)
// Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type csharp(CS1662)
foo.Match<OneOf<X, Y>>(
    a => a.toX()
    b => b.toY()
    c => c.toY());

// l is OneOf<X, Y, Y>
var l = foo
    .MapT0(a => a.toX())
    .MapT1(b => b.toY())
    .MapT2(c => c.toY());

Extra Utility Methods

I think there are a couple of useful methods that could be useful in this library, specifically for the OneOf Struct.

The two I am thinking of would be

  1. Return
  2. Map

Even though there are implicit converters, that approach fails when using interfaces. There are two ways I have typically gotten around that by implementing OneOf<T1, T2..> Return (T1 input) => input but might be a good addition. The other is to implement a class which holds the Interface inside of it to side step the issue, making the first workaround part of OneOf itself might be a nice change as I have found myself wanting to have a way to construct OneOf without having to rely on a typed variable.

Map would Take a OneOf<T1,T2 ...> and Func<T1, TResult> with different overloads for each Generic type we would want to replace and the result would be OneOf<TResult, T2..>

I'm a big fan of this library and it has started showing up all over the codebase I work on, I would like to know if you are interested in this kind of change.

[Perf] Redundant checks in a few places

A few of the methods are double checking which index is set or making redundant nested calls that could be removed
eg:

public OneOf<TResult, T1, T2> MapT0<TResult>(Func<T0, TResult> mapFunc)
{
    if (mapFunc == null)
    {
        throw new ArgumentNullException(nameof(mapFunc));
    }
    return _index switch
    {
        0 => mapFunc(AsT0), // could be: mapFunc(_value0)
        1 => AsT1, // _value1
        2 => AsT2, // _value2
        _ => throw new InvalidOperationException()
    };
}

or

public bool TryPickT0(out T0 value, out OneOf<T1, T2> remainder)
{
    value = IsT0 ? AsT0 /*_value0*/ : default;
    remainder = _index switch
    {
        0 => default,
        1 => AsT1, // _value1
        2 => AsT2, // _value2
        _ => throw new InvalidOperationException()
    };
    return isT0;
}

Considering that AsTn properties are doing extra checks and not simple props, it won't allow compiler to optimize them either.

I am more than happy to send a PR, if this change is something you would like to incorporate.

Lambda syntax to OneOf of delegate types

The following:

OneOf<
    Func<int, int>,
    Func<string, string>
> del = (int i) => i;

does not compile, with:

Cannot convert lambda expression to type 'OneOf<Func<int, int>,Func<string, string>' because it is not a delegate type.

I can work around this with the following:

OneOf<
    Func<int, int>,
    Func<string, string>
> del = (Func<int, int>)((int i) => i);

or

var del = OneOf<
    Func<int, int>,
    Func<string, string>
>.FromT0(i => i);

But is there some way for OneOf to figure out that all the subtypes are delegate types, and pass this information to the compiler for an implicit conversion?

Use a Source Generator to implement OneOfBase heirarchies

This is a placeholder issue. If anyone would like to contribute this feature, it would be very welcome!

Proposal

It would be great to have a source generator automatically implement OneOfBase hierarchies, so

using OneOf;

[GenerateOneOf]
public partial class StringOrNumber : IOneOf<string, int>{ }

would also cause a file containing something like:

public partial class StringOrNumber : OneOfBase<string, int>
{
    StringOrNumber(OneOf<string, int> _) : base(_) { }
    public static implicit operator StringOrNumber(string _) => new StringOrNumber(_);
    public static implicit operator StringOrNumber(int _) => new StringOrNumber(_);
}

to be output.

https://github.com/RyotaMurohoshi/ValueObjectGenerator would be a good starting point for this

OneOfBase Source Generation

I am having trouble with source generation with OneOfBase and am wondering if I am missing something.

For example, given:

public partial class StringOrNumber : OneOfBase<string, int> { }

The above code, taken from the readme, does not compile, with error:
CS7036: There is no argument given that corresponds to the required formal parameter 'input' of 'OneOfBase<string, int>.OneOfBase(OneOf<string, int>)

To make that go away I have to add a constructor, such as:
public StringOrNumber(OneOf<string, int> input) : base(input) { }

But then the code generator fails because it wants to create the constructor.

Messing around with named fields

Thought you might find this interesting. I was messing around with OneOf and wishing its values could be named similar to ValueTuple in C# 7.

I think this is the closest you can get with C# 7:

var x = OneOf<(int Error, int Foo, object Bar)>.Create(_ => _.Foo, 0);
            
x.Switch(
    error =>
    {
        throw new Exception(error.ToString());
    },
    foo =>
    {
        DoFoo(foo);
    },
    bar =>
    {
        DoBar(bar);
    });

var result = x.Match(
    error => error,
    foo => foo * 2,
    bar => 3);

Implementation:

public struct OneOf<TTuple> where TTuple : struct, IEquatable<TTuple>, IStructuralEquatable, IStructuralComparable, IComparable, IComparable<TTuple>
{
    internal readonly int FieldNumber;
    internal readonly object Value;
    
    private OneOf(int fieldNumber, object value)
    {
        FieldNumber = fieldNumber;
        Value = value;
    }

    public static OneOf<TTuple> Create<TValue>(Expression<Func<TTuple, TValue>> field, TValue value)
    {
        if (field.Body is MemberExpression memberExpression
            && memberExpression.Expression == field.Parameters[0]
            && memberExpression.Member.Name.StartsWith("Item", StringComparison.Ordinal)
            && int.TryParse(memberExpression.Member.Name.Substring(4), out int fieldNumber))
        {
            return new OneOf<TTuple>(fieldNumber, value);
        }

        throw new InvalidOperationException("The field must be specified as direct access to the tuple field or property.");
    }
}

public static class OneOfExtensions
{
    public static void Switch<T1, T2, T3>(this OneOf<ValueTuple<T1, T2, T3>> oneOf, Action<T1> field1, Action<T2> field2, Action<T3> field3)
    {
        switch (oneOf.FieldNumber)
        {
            case 1:
                field1.Invoke((T1)oneOf.Value);
                break;
            case 2:
                field2.Invoke((T2)oneOf.Value);
                break;
            case 3:
                field3.Invoke((T3)oneOf.Value);
                break;
            default:
                throw new InvalidOperationException("Struct was not initialized properly.");
        }
    }

    public static TResult Match<T1, T2, T3, TResult>(this OneOf<ValueTuple<T1, T2, T3>> oneOf, Func<T1, TResult> field1, Func<T2, TResult> field2, Func<T3, TResult> field3)
    {
        switch (oneOf.FieldNumber)
        {
            case 1:
                return field1.Invoke((T1)oneOf.Value);
            case 2:
                return field2.Invoke((T2)oneOf.Value);
            case 3:
                return field3.Invoke((T3)oneOf.Value);
            default:
                throw new InvalidOperationException("Struct was not initialized properly.");
        }
    }
}

It's not high perf and it's probably not exactly the direction I want to go, but I wanted to document it in case it triggers someone's imagination.

Add exception messages

  • OneOf.Value when _index is out of bounds
  • (Perhaps) Agument OneOf.AsTx to output the types (e.g. Cannot return as T0 (System.String) as T2 (System.Int32))
  • OneOf.Switch (should probably distinguish _index out of bounds vs f0 being null)
  • OneOf.Match (should probably distinguish _index out of bounds vs f0 being null)

Question: How to use with async / await?

Question: is there a way to call Match with async / await? Related to #26

I have:

public async Task<OneOf<Foo, None>> HandleAsync(GetFooQuery query)
{
	var foo = await GetByIdAsync(query.FooId);

	return foo.Match<OneOf<Foo, None>>(
		f => GetCustomerNameForFooAsync(f).Result,
		_ => new None());
}

private async Task<OneOf<Foo, None>> GetByIdAsync(FooId fooId)
{ ...

private async Task<Foo> GetCustomerNameForFooAsync(Foo foo)
{ ...

but the .Result on the end is a bit icky (edit: and is not asyncronous!). Would be nice if I could do something like:

public async Task<OneOf<Foo, None>> HandleAsync(GetFooQuery query)
{
	var foo = await GetByIdAsync(query.FooId);

	return foo.Match<OneOf<Foo, None>>(
		async f => await GetCustomerNameForFooAsync(f),
		_ => new None());
}

Turn OneOf into an unsealed class instead of a struct in order to use inhereitence for creating names

Consider the following code:

public class Success<T> : ValueOf<T, Success<T>> { }
public class Retry : ValueOf<TimeSpan, Retry> { }
public class Abort : ValueOf<string, Abort> { }
    
public class Result<T> : OneOf<Success<T>, Retry, Abort> { }

The last line of course doesn't work, as OneOf is a struct.

Two reasons to turn OneOf into a class:

  1. It'll be less chaotic for the consumer of the result to read - they shouldn't care how the result they're received was implemented.
  2. Aliases have to be fully resolved, so they cannot contain generics. That means that the following alias wouldn't be valid.
using Result<T> = OneOf<Success<T>, Retry, Abort>;

So even if you just want to use the shorthands internally, it's not an option to use aliases + generics.

Let me know if you think this is a worthwhile endevor

File size

Somewhere between 2.1.90 and now, the assembly size has increased from 63 kilobytes to 10.1 megabytes.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.