3
\$\begingroup\$

I have code that basically is the same, but depending on the number of arguments you provide (up to 5), it will operate with them in a very repetitive way. I have the feeling that this can be optimized in some way, since maintaining and creating all the overloads by hand is a tedious and error-prone task.

The code is 5 versions (overloads) of the Operate method. As you can see, it increasingly deal with 2, 3, 4 and 5 arguments (each one is of a given type), so it can adapt from 2 to 5 type arguments.

public static Either<ErrorList, TResult> Operate<T1, T2, TResult>(Either<ErrorList, T1> a, Either<ErrorList, T2> b,
    Func<T1, T2, Either<ErrorList, TResult>> onSuccess)
{
    var success = from av in a.RightValue
                  from bv in b.RightValue
                  select onSuccess(av, bv);
    var errorList = new[]
        {
            a.LeftValue,
            b.LeftValue,
        }
        .SelectMany(x => x.ToEnumerable())
        .Aggregate((x, y) => new ErrorList(x.Concat(y)));

    return success.ValueOr(() => errorList);
}

public static Either<ErrorList, TResult> Operate<T1, T2, T3, TResult>(Either<ErrorList, T1> a,
    Either<ErrorList, T2> b, Either<ErrorList, T3> c,
    Func<T1, T2, T3, Either<ErrorList, TResult>> onSuccess)
{
    var success =
        from av in a.RightValue
        from bv in b.RightValue
        from cv in c.RightValue
        select onSuccess(av, bv, cv);

    var errorList = new[]
        {
            a.LeftValue,
            b.LeftValue,
            c.LeftValue,
        }
        .SelectMany(x => x.ToEnumerable())
        .Aggregate((x, y) => new ErrorList(x.Concat(y)));

    return success.ValueOr(() => errorList);
}

public static Either<ErrorList, TResult> Operate<T1, T2, T3, T4, TResult>(Either<ErrorList, T1> a, Either<ErrorList, T2> b, Either<ErrorList, T3> c, Either<ErrorList, T4> d,
    Func<T1, T2, T3, T4, Either<ErrorList, TResult>> onSuccess)
{
    var success = from av in a.RightValue
                  from bv in b.RightValue
                  from cv in c.RightValue
                  from dv in d.RightValue
                  select onSuccess(av, bv, cv, dv);

    var errorList = new[]
        {
            a.LeftValue,
            b.LeftValue,
            c.LeftValue,
            d.LeftValue,
        }
        .SelectMany(x => x.ToEnumerable())
        .Aggregate((x, y) => new ErrorList(x.Concat(y)));

    return success.ValueOr(() => errorList);
}

public static Either<ErrorList, TResult> Operate<T1, T2, T3, T4, T5, TResult>(
    Either<ErrorList, T1> a, 
    Either<ErrorList, T2> b, 
    Either<ErrorList, T3> c, 
    Either<ErrorList, T4> d,
    Either<ErrorList, T5> e,
    Func<T1, T2, T3, T4, T5, Either<ErrorList, TResult>> onSuccess)
{
    var success = from av in a.RightValue
        from bv in b.RightValue
        from cv in c.RightValue
        from dv in d.RightValue
        from ev in e.RightValue
        select onSuccess(av, bv, cv, dv, ev);

    var errorList = new[]
        {
            a.LeftValue,
            b.LeftValue,
            c.LeftValue,
            d.LeftValue,
            e.LeftValue,
        }
        .SelectMany(x => x.ToEnumerable())
        .Aggregate((x, y) => new ErrorList(x.Concat(y)));
    
    return success.ValueOr(() => errorList);
}

Is there a better way to do this without repeating almost the same code? Imagine that I would like to create another overload for 6, 7, 8... arguments. It can be really tricky and sub-optimal.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ You may use the (generic) Template Method design pattern, to concentrate the repeated algorithmic parts in a base class. \$\endgroup\$ Aug 28, 2020 at 11:05
  • 2
    \$\begingroup\$ I'd suggest you use a T4 template (.tt file) to generate the overloads. You can have a for loop from 2 up to the number of arguments you need and generate each overload. The T4 template itself might be a bit of work but I think it should be straightforward. \$\endgroup\$
    – RobH
    Aug 28, 2020 at 11:57
  • \$\begingroup\$ How do you use this code right now? \$\endgroup\$ Aug 28, 2020 at 13:42

1 Answer 1

1
\$\begingroup\$

You can try to extract the common piece of code into a helper method:

private static Either<ErrorList, TResult> Operate<TResult>(ErrorList[] errorLists, Either<ErrorList, TResult> success)
{
    var errorList = errorLists
        .SelectMany(x => x.ToEnumerable())
        .Aggregate((x, y) => new ErrorList(x.Concat(y)));

    return success.ValueOr(() => errorList);
}

Then you can reduce the overloads' body only to calculate the helpers inputs:

public static Either<ErrorList, TResult> Operate<T1, T2, T3, T4, T5, TResult>(
    Either<ErrorList, T1> a, Either<ErrorList, T2> b, Either<ErrorList, T3> c, Either<ErrorList, T4> d, Either<ErrorList, T5> e,
    Func<T1, T2, T3, T4, T5, Either<ErrorList, TResult>> onSuccess)
  => Operate<TResult>(
    new [] { a.LeftValue, b.LeftValue, c.LeftValue, d.LeftValue, e.LeftValue },
    onSuccess(a.RightValue, b.RightValue, c.RightValue, d.RightValue, e.RightValue));
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.