In C# 7 is it possible to deconstruct tuples as method arguments

For example I have

private void test(Action<ValueTuple<string, int>> fn)
{
    fn(("hello", 10));
}

test(t => 
 {
    var (s, i) = t;
    Console.WriteLine(s);
    Console.WriteLine(i);
});

I would like to write something like this

private void test(Action<ValueTuple<string, int>> fn)
{
    fn(("hello", 10));
}

test((s,i) => 
{
    Console.WriteLine(s);
    Console.WriteLine(i);
});

Is this possible with some proper notation?


Solution 1:

You can shorten it to:

void test( Action<ValueTuple<string, int>> fn)
{
    fn(("hello", 10));
}

test(((string s, int i) t) =>
{
    Console.WriteLine(t.s);
    Console.WriteLine(t.i);
});

Hopefully, one day we might be able to splat the parameters from a tuple to the method invocation:

void test(Action<ValueTuple<string, int>> fn)
{
    fn(@("hello", 10)); // <-- made up syntax
}

test((s, i) =>
{
    Console.WriteLine(s);
    Console.WriteLine(i);
});

But not at the moment.

Solution 2:

I. Examples of Action /Func delegates with distinct-args vs. single n-tuple arguments:

// 1. Action with 3 distinct 'int' parameters
Action<int, int, int> ArgsAction = (i1, i2, i3) => i1 += i2 += i3;

// 2. Func with 3 distinct 'int' parameters, returning 'long'
Func<int, int, int, long> ArgsFunc = (i1, i2, i3) => (long)i1 + i2 + i3;

// 3. Action with a single 3-tuple parameter
Action<(int, int, int)> TupleAction = args => args.Item1 += args.Item2 += args.Item3;

// 4. Action with a single 3-tuple parameter, returning 'long'
Func<(int, int, int), long> TupleFunc = args => (long)args.Item1 + args.Item2 + args.Item3;

II. Demonstrate direct usage of the above examples

long r;

// pass distinct params to multi-arg methods

ArgsAction(1, 2, 3);                // 1.

r = ArgsFunc(1, 2, 3);              // 2.

// pass tuple to tuple-taking methods

TupleAction((1, 2, 3));             // 3.

r = TupleFunc((1, 2, 3));           // 4.

The examples in the next two sections invoke the delegates in their respective non-native argument forms. To delay the method call or to retain an adapted delegate for caching or delayed/multiple-call secenarios, see VI. and VII.

III. disperse ("splat") a tuple into multi-arg methods.

(1, 2, 3).Scatter(ArgsAction);      // 1.

r = (1, 2, 3).Scatter(ArgsFunc);    // 2.

IV. pass distinct args into tuple-taking methods:

TupleAction.Gather(1, 2, 3);        // 3.

r = TupleFunc.Gather(1, 2, 3);      // 4.

V. Extension methods Scatter and Gather used above in (III) and (IV):

// disperse n-tuple into Action arguments
public static void Scatter<T0, T1>(in this (T0 i0, T1 i1) t, Action<T0, T1> a) => a(t.i0, t.i1);
public static void Scatter<T0, T1, T2>(in this (T0 i0, T1 i1, T2 i2) t, Action<T0, T1, T2> a) => a(t.i0, t.i1, t.i2);
public static void Scatter<T0, T1, T2, T3>(in this (T0 i0, T1 i1, T2 i2, T3 i3) t, Action<T0, T1, T2, T3> a) => a(t.i0, t.i1, t.i2, t.i3);

// disperse n-tuple into Func arguments
public static TResult Scatter<T0, T1, TResult>(in this (T0 i0, T1 i1) t, Func<T0, T1, TResult> f) => f(t.i0, t.i1);
public static TResult Scatter<T0, T1, T2, TResult>(in this (T0 i0, T1 i1, T2 i2) t, Func<T0, T1, T2, TResult> f) => f(t.i0, t.i1, t.i2);
public static TResult Scatter<T0, T1, T2, T3, TResult>(in this (T0 i0, T1 i1, T2 i2, T3 i3) t, Func<T0, T1, T2, T3, TResult> f) => f(t.i0, t.i1, t.i2, t.i3);

// accumulate 'n' distinct args and pass into Action as an n-tuple
public static void Gather<T0, T1>(this Action<(T0, T1)> a, T0 i0, T1 i1) => a((i0, i1));
public static void Gather<T0, T1, T2>(this Action<(T0, T1, T2)> a, T0 i0, T1 i1, T2 i2) => a((i0, i1, i2));
public static void Gather<T0, T1, T2, T3>(this Action<(T0, T1, T2, T3)> a, T0 i0, T1 i1, T2 i2, T3 i3) => a((i0, i1, i2, i3));

// accumulate 'n' distinct args and pass into Func as an n-tuple
public static TResult Gather<T0, T1, TResult>(this Func<(T0, T1), TResult> f, T0 i0, T1 i1) => f((i0, i1));
public static TResult Gather<T0, T1, T2, TResult>(this Func<(T0, T1, T2), TResult> f, T0 i0, T1 i1, T2 i2) => f((i0, i1, i2));
public static TResult Gather<T0, T1, T2, T3, TResult>(this Func<(T0, T1, T2, T3), TResult> f, T0 i0, T1 i1, T2 i2, T3 i3) => f((i0, i1, i2, i3));

VI. Bonus round. If you plan to call a tuple- or distinct-arg-taking delegate multiple times in its alternate form, or if you're not ready to actually invoke it yet, you may wish to explicitly pre-convert the delegate from tuple-taking form to the equivalent distinct-args delegate, or vice-versa. You can cache the converted delegate for multiple or arbitrary later re-use.

var ga = ArgsAction.ToGathered();        // 1.
// later...
ga((1, 2, 3));
// ...
ga((4, 5, 6));

var gf = ArgsFunc.ToGathered();          // 2.
// later...
r = gf((1, 2, 3));
// ...
r = gf((4, 5, 6));

var sa = TupleAction.ToScattered();      // 3.
// later...
sa(1, 2, 3);
// ...
sa(4, 5, 6);

var sf = TupleFunc.ToScattered();        // 4.
// later...
r = sf(1, 2, 3);
// ...
r = sf(4, 5, 6);

// of course these approaches also supports in-situ usage:

ArgsAction.ToGathered()((1, 2, 3));      // 1.
r = ArgsFunc.ToGathered()((1, 2, 3));    // 2.

TupleAction.ToScattered()(1, 2, 3);      // 3.
r = TupleFunc.ToScattered()(1, 2, 3);    // 4.

VII. Extension methods for bonus examples shown in VI.

// convert tuple-taking Action delegate to distinct-args form
public static Action<T0, T1> ToScattered<T0, T1>(this Action<(T0, T1)> a) => (i0, i1) => a((i0, i1));
public static Action<T0, T1, T2> ToScattered<T0, T1, T2>(this Action<(T0, T1, T2)> a) => (i0, i1, i2) => a((i0, i1, i2));
public static Action<T0, T1, T2, T3> ToScattered<T0, T1, T2, T3>(this Action<(T0, T1, T2, T3)> a) => (i0, i1, i2, i3) => a((i0, i1, i2, i3));

// convert tuple-taking Func delegate to its distinct-args form
public static Func<T0, T1, TResult> ToScattered<T0, T1, TResult>(this Func<(T0, T1), TResult> f) => (i0, i1) => f((i0, i1));
public static Func<T0, T1, T2, TResult> ToScattered<T0, T1, T2, TResult>(this Func<(T0, T1, T2), TResult> f) => (i0, i1, i2) => f((i0, i1, i2));
public static Func<T0, T1, T2, T3, TResult> ToScattered<T0, T1, T2, T3, TResult>(this Func<(T0, T1, T2, T3), TResult> f) => (i0, i1, i2, i3) => f((i0, i1, i2, i3));

// convert distinct-args Action delegate to tuple-taking form
public static Action<(T0, T1)> ToGathered<T0, T1>(this Action<T0, T1> a) => t => a(t.Item1, t.Item2);
public static Action<(T0, T1, T2)> ToGathered<T0, T1, T2>(this Action<T0, T1, T2> a) => t => a(t.Item1, t.Item2, t.Item3);
public static Action<(T0, T1, T2, T3)> ToGathered<T0, T1, T2, T3>(this Action<T0, T1, T2, T3> a) => t => a(t.Item1, t.Item2, t.Item3, t.Item4);

// convert distinct-args Func delegate to its tuple-taking form
public static Func<(T0, T1), TResult> ToGathered<T0, T1, TResult>(this Func<T0, T1, TResult> f) => t => f(t.Item1, t.Item2);
public static Func<(T0, T1, T2), TResult> ToGathered<T0, T1, T2, TResult>(this Func<T0, T1, T2, TResult> f) => t => f(t.Item1, t.Item2, t.Item3);
public static Func<(T0, T1, T2, T3), TResult> ToGathered<T0, T1, T2, T3, TResult>(this Func<T0, T1, T2, T3, TResult> f) => t => f(t.Item1, t.Item2, t.Item3, t.Item4);

Solution 3:

There are two ways of looking at your request, but neither is supported in C# 7.0.

  • One is splatting of tuples into arguments: calling a method with a tuple, and have the elements of the tuple splat into distinct arguments of the method. You can do this today manually by invoking M(tuple.first, tuple.second).
  • The other is deconstruction in lambda parameters: when a lambda is invoked with a parameter, deconstruct that parameters into elements and use those elements in the lambda body. You can do this today manually by defining a lambda as x => { var (first, second) = x; Write(first); Write(second); }.

There are proposals being discussed in the csharplang design repo.

  • https://github.com/dotnet/csharplang/issues/424 (splatting of tuples into arguments)
  • https://github.com/dotnet/csharplang/issues/258 (deconstruction in lambda parameters)

Solution 4:

One option is to use TupleSplatter (https://github.com/chartjunk/TupleSplatter):

using TupleSplatter;

void test(Action<string, int> fn)
{
    fn.SplatInvoke(("hello", 10));
    // or
    ("hello", 10).Splat(fn);
}

test((s,i) => {
    Console.WriteLine(s);
    Console.WriteLine(i);
});

Solution 5:

Here are more concise syntax variations that don't require any extra imports. No, it does not resolve wishes for "splatter" syntax discussed in comments, but no other answer used the ValueTuple syntax for the initial parameter definition.

void test(Action<(string, int)> fn)
{
    fn(("hello", 10));
}

// OR using optional named ValueTuple arguments
void test(Action<(string word, int num)> fn)
{
    fn((word: "hello", num: 10));
}

The invocation using a lambda expression is not that verbose, and the ValueTuple components can still be retrieved using minimal syntax:

test( ((string, int) t) => {
    var (s, i) = t;

    Console.WriteLine(s);
    Console.WriteLine(i);
});