mcintyre321 / OneOf

Easy to use F#-like ~discriminated~ unions for C# with exhaustive compile time matching
MIT License
3.51k stars 163 forks source link

Intended usage when calling method that itself return a OneOf<T> #145

Open BADF00D opened 1 year ago

BADF00D commented 1 year ago

I'm new to this library and not quite sure if I use it the right way.

Imagine I want to write a little Parser, that returns a record of type Result. Each property is parsed by invoking a method that can parse this property or returns an exception. How would I write the Parse method, that collects the values from the methods?

My current solution looks like the following:

public record Result(string Name, int Age, Guid Id);
public class Parser
{
    public OneOf<Result, Exception> Parse()
    {
        return GetName()
            .Match(name => GetAge()
                .Match(age => GetId()
                    .Match(
                        id => OneOf<Result, Exception>.FromT0(new Result(name, age, id)), 
                        e => e), 
                    e => e), 
                e => e);
    }

    private OneOf<string, Exception> GetName()  => "Value1";
    private OneOf<int, Exception> GetAge() => 123;
    private OneOf<Guid, Exception> GetId() => Guid.NewGuid();
}

Is there a better, more readable way to write the Parse method?

ghostnguyen commented 1 year ago

You can try the Bind ext method.

public record Result(string Name, int Age, Guid Id);

public class Parser
{
    public OneOf<Result, Exception> Parse()
    {
        return GetName()
            .Combind(GetAge())
            .Combind2(GetId())
            .MapT0(_ => new Result(_.Item1, _.Item2, _.Item3));
    }

    private OneOf<string, Exception> GetName() => "Value1";

    private OneOf<int, Exception> GetAge() => 123;

    private OneOf<Guid, Exception> GetId() => Guid.NewGuid();
}

public static class Ext
{
    public static OneOf<(T1, T2), E> Combind<T1, T2, E>(this OneOf<T1, E> opt1, OneOf<T2, E> opt2)
        => opt1.Match(
            t1 => opt2.Match(
                t2 => OneOf<(T1, T2), E>.FromT0((t1, t2)),
                e => e
                ),
            e => e
            );

    public static OneOf<(T1, T2, T3), E> Combind2<T1, T2, T3, E>(this OneOf<(T1, T2), E> opt1, OneOf<T3, E> opt2)
        => opt1.Combind(opt2).MapT0(_ => (_.Item1.Item1, _.Item1.Item2, _.Item2));
}
emperador-ming commented 1 year ago

Is there a better, more readable way to write the Parse method?

@BADF00D, enter the monad!


#r "nuget: OneOf, 3.0.243"

using OneOf;

public class Parser
{
    public OneOf<Result, Exception> Parse() =>
        GetName()
            .AsBindable()
            .Bind(name => new Result(name).AsBindable())
            .Bind(result => GetAge().AsBindable().Bind(age => (result with { Age = age }).AsBindable()))
            .Bind(result => GetId().AsBindable().Bind(id => (result with { Id = id }).AsBindable()))
            .Result();

    private OneOf<string, Exception> GetName()  => "Value1";
    private OneOf<int, Exception> GetAge() => 123;
    private OneOf<Guid, Exception> GetId() => Guid.NewGuid();
}

// Monad
public class Maybe<TA> where TA: notnull
{
    private OneOf<TA, Exception> value;

    private Maybe(Exception ex) => value = ex;

    public Maybe(TA someValue) => value = someValue;

    public Maybe(OneOf<TA, Exception> value) => this.value = value;

    public Maybe<TB> Bind<TB>(Func<TA, Maybe<TB>> f) where TB : notnull
        => value.Match(
                taValue => f(taValue),
                ex => Maybe<TB>.Exception(ex));

    public static Maybe<TA> Exception(Exception ex) => new Maybe<TA>(ex);
    public OneOf<TA, Exception> Result() => value;
}

// Extension methods
public static Maybe<TA> AsBindable<TA>(this TA value) where TA : notnull
    => new Maybe<TA>(value);

public static Maybe<TA> AsBindable<TA>(this OneOf<TA, Exception> oneof) where TA : notnull
    => new Maybe<TA>(oneof);