Given `Output<A>, Output<B>, Func<A...
# dotnet
e
Given
Output<A>, Output<B>, Func<A,B,C>
and wanting to get
Output<C>
is the least verbose option
Output.Tuple(a,b).Apply(x => func(x.Item1, x.Item2)
and specifically are there reflection or codegen helpers I’m missing to generalize this on records to convert between records where properties are typed T1,..TN and records where properties are typed
In/Output<T1>..In/Output<TN>
? Working on some codegen helpers here and my current attempt looks like this: https://gist.github.com/t0yv0/a9139fd04f28a5d4e8f1cf20909023ae#file-codegen-cs-L21
w
Maybe @tall-librarian-49374 has some tricks up his sleeve otherwise looks about right. FWIW, if you want the tuple to be more readable you can use semantic names (and types) https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7#tuples-and-discards You can also use an expression body instead of a statement body https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/expression-bodied-members#methods:
Copy code
public static Output<ListStorageAccountKeysResult> Apply(ListStorageAccountKeysApplyArgs args, InvokeOptions? options = null) =>
    Output.Tuple(args.AccountName, args.Expand, args.ResourceGroupName)
        .Apply(((string AccountName, string Expand, string ResourceGroupName)tuple) => InvokeAsync(
            new ListStorageAccountKeysArgs
            {
                AccountName = tuple.AccountName,
                Expand = tuple.Expand,
                ResourceGroupName = tuple.ResourceGroupName
            }, options));
t
I think that’s what we have so far. Also,
Tuple
is limited to 8 parameters.
👍 1
e
Thanks I’ll proceed with this approach for now.
w
I still find the tuple helpers quite clunky, especially if you want to use names on the other side. I was just wondering if anonymous types could help here for something like:
Copy code
public static Output<ListStorageAccountKeysResult> Apply(ListStorageAccountKeysApplyArgs args, InvokeOptions? options = null) =>
    new { args.AccountName, args.Expand, args.ResourceGroupName }.ToOutput() // magic here
        .Apply(anon => InvokeAsync(
            new ListStorageAccountKeysArgs
            {
                AccountName = anon.AccountName,
                Expand = anon.Expand,
                ResourceGroupName = anon.ResourceGroupName
            }, options));
e
I’ve opted for boxig/unboxing in the end, since ultimately it’s an impl detail and not exposed to the user. This has been the bane of me for a couple of days… Initial attempts to use
Tuple
packed into max-8 trees ran the limit of C# refusing to do type inference on these (I miss F#). You can see the current candidate implementation here https://github.com/pulumi/pulumi/pull/7087/files#diff-009df8f241c8e473d30f34b62a89229ed7fbbc2b89f3e4404794184a1db06887
w
More thoughts... anonymous types would mean you could replace
Output.All
with awkward indexing of the lifted result:
Copy code
Output.All(args.A, args.B).Apply(array => array[0] ... array[1] etc)
with:
Copy code
Output.Anon(new { args.A, args.B }).Apply(anon => anon.A ... anon.B etc)
which would need an output apply pipeline like:
Copy code
Output.Anon(a').Apply(b')
where a' and b' are anonymous types and b' has the same shape (property names) as a' except all the types are the "lifted" types of a'
Since anonymous types are compiler syntactic sugar, I wonder if this is possible using source generators? (cookbook) 🤔
I guess it would need to generate the equivalent extension method: <https://github.com/pulumi/pulumi/blob/8a4b0f7cb64534cafa91ee5416179b03c0d36879/sdk/dotnet/Pulumi/Core/Output.cs#L197-L198%7COutput&lt;U> Output.Apply<U>(this Output output, Func<T, U> func)> where T is typeof a' and U is typeof b'
No, that's not quite right, but maybe you get where I'm coming from and can see a way through... Thoughts, @enough-garden-22763 @tall-librarian-49374?
e
Are you thinking to generally improve experience for users that try to join a few outputs together?
Sorry I think my brain is permanently fried for today 🙂 I will have to revisit another day to give this justice.
w
Are you thinking to generally improve experience for users that try to join a few outputs together?
Yes! 😀 Both
Output.All
and
Output.Tuple
are clumsy to use because you lose property name access on the way through.
e
One idea I had there, though need to formalize a bit more, would be like this:
Copy code
Output.Combine(v => v.Eval(x) + v.Eval(y))
Copy code
interface IOutputEvaluator {
   T Eval<T>(Output<T> output);
}

Output<T> Output.Combine<T>(f: Func<IOutputEvaluator,T>)
This makes it a little less clumsy for me as the name of whatever output variable you have is close at hand. However this proposal of mine has a big downside so I haven’t formalized it yet as I expect it to be shot down 🙂 The downside is that an implementation of this interface treats
f
arg not as a real function but more as a “magic” function which only works for reduced instructions. The user is not allowed to branch on the results of outputs or conditionally evaluate an output. Since the implementation introspects
f
by evaluating it several times.. Side-effects are also not allowed.
This becomes almost too much to explain, and can be very confusing if these assumptions are violated.
w
An anonymous type provides a convenient way to quickly combine, and possibly rename, a bunch of values which should be treated as input values, not limited to 8 slots etc. I've seen similar in the pulumi code base, although the magic sauce needed is to be able to project another anonymous type, with the same property names, but whose values will all be converted to outputs (as required) and subsequently awaited under the hood, so that you can access the "lifted" future values in apply. Similar to AllHelperAsync or TupleHelperAsync but for anonymous types.