Why does pulumi have this concept of Output monad ...
# general
b
Why does pulumi have this concept of Output monad instead of just using promises in TS/JS... Promises have a well understood api, and provide syntactical sugar for asynchronous control-flow with the async and await keywords, also supports resolving multiple promises concurrently using Promise.all function. Why use something like the Output class instead of getting all this for free?
b
because promises don’t exist in all languages. Promises don’t exist in Python, Dotnet or Go.
Pulumi strives to be a multi language solution and adopting a single language solution like promises restricts adoptability
e
Also Output does strictly more than a promise. It also tracks resource dependencies, secretness flags, and handles the case during preview when values are unknown.
r
I understand there are theoretical and API differences between JavaScript Promises, .NET Tasks, whatever Python uses behind `async`/`await`, and goroutines, etc., and Fraser’s point that an
Output
is much more than just the underlying asynchronous control flow implementation, but… each of these implementations provides an idiomatic interface for that language. The language agnosticism is wonderful, of course, but aren’t `Output`s built on the language’s underlying primitive anyway? (I know they are in the .NET and Node SDKs, at least.) Exposing the primitive’s interface for `Output`s--i.e. being able to
await
an
Output
in Node, .NET, and Python--would be a wonderful usability improvement ☺️
e
You could await an Output<T> but the result wouldn't be a T. It would be something like (maybeValue Option<T>, isSecret bool, dependencies URN[]), plus potentially more as we track more stuff back and forth from the SDK to the engine (for example I've been thinking about changing outputs to track if they are plain field references, so that we can make update plans stricter) We don't think that anyone wants to deal with that complexity, and that if they think they do want to deal with that complexity they're in most cases going to get it wrong (and then tell us that Pulumi has bugs), and it's not an interface we want to commit to keeping stable (see comment above about tracking more data via Outputs). I'd be fine (and I think our Go SDK already has this) adding an advanced
OutputToPromise
or similar function that you could just await, but it would require you to handle all the complexity above, and to acknowledge that more complexity could be added to that state as the engine evolves. What would be far more interesting is if these languages could develop a way to let us write awaits for things that aren't promises. So F# for example does await via computation expressions, and we can make outputs look exactly the same as Async's in F#. But most other languages have made async/await specific to their promise type with no way to hook user defined types into the same syntax. Which is a bit sad, it's all just monads really.
r
That makes sense, thank you—especially for replying at the weekend! 😳 We have something along these lines in our internal code library, based on the C# unit testing examples:
Copy code
public static class OutputTaskExtensions
{
    public static ConfiguredTaskAwaitable<TResult>.ConfiguredTaskAwaiter GetAwaiter<TResult>(this Output<TResult> output)
    {
        TaskCompletionSource<TResult> dataCompletion = new();
        output.Apply(o =>
        {
            dataCompletion.SetResult(o);
            return o;
        });

        return dataCompletion.Task.ConfigureAwait(false).GetAwaiter();
    }
}
which does allow us to directly
await
an
Output<T>
in C#. I’ve seen it deadlock in our tests, though (usually when the mocks haven’t been configured with a dependent output), which makes me uncomfortable using it in a deployable Stack. It would be great if the
Output<T>
were able to use this extension method approach in the dotnet SDK. I’ve also done something similar in TypeScript (I forget the precise syntax now, sorry):
Copy code
new Promise(resolve => output.apply(_ => resolve))
to wrap the
Output<T>
in an awaitable Promise. I guess this has the problems you described of having to deal with the underlying detail of the Output type, though?
e
I guess this has the problems you described of having to deal with the underlying detail of the Output type, though?
Yup. Firstly
apply
might never run (we don't run applys if the value is "unknown" at preview time) so you have to deal with that promise never resolving, probably why you've been seeing deadlocks. If we built a "ToPromise" method into the SDK we could make it always resolve, but then it might not always resolve to a value (e.g. you'd get like
T | undefined
). And then secondly your losing all the information about secretness, dependencies, anything else we start tracking, etc. Again if we made this a proper part of the SDK we'd return a structure with all that information on it, but it would be up you the user to make use of it correctly. While with
apply
we handle all the details of that extra data. An await function has got its uses (mostly working around tricky bugs we haven't fixed yet, which is why one Go customer asked for it to be added to the SDK there), but I'd definitely class it as an advanced feature.
b
Hi, I have a question here, Would it be possible to know if the ressource is known when running preview? It would be something like:
if (pulumi.isDryRun() && my_ressource.isUnknown()) {// manage special case for my preview))
e
I don't think we have anything builtin to directly support that now, we generally advise not trying to handle unknowns explictly and just let those data flows stay as unknown otherwise you can very easily get diverging behaviour between preview and update. But we are adding some unsafe advanced "await this output even if it's unknown" functions to the SDKs to unlock advanced scenarios not currently safely covered by the built in methods, so you could use that. But it might be that an "output.ifUnknown(() => ...)" function would make sense for these, and would be a lot safer to not screw up. Could you raise an issue at https://github.com/pulumi/pulumi/issues with some more details of your use case and we can consider it.
b
Ok I will do. I am also interested into the other method you are mentioning.
e
So the Go SDK has an UnsafeAwaitOutput. We're planning on bringing the other SDKs inline with Go and adding something like that all the SDKs. That lets you await for the value from an output, or the "unknown" result. So obviously you'd be able to use something like that to change behaviour on unknowns. But as it says, it's not exactly safe. It's on you to correctly propagate all the other information contained on an Ouput. An "ifUnknown" function would be safeish, obv can cause horrible divergences between preview and update but could at least maintain secret and dependency tracking for you.
b
Yes if it safer, I would work with the “ifUnknown”. The developer has to manage on her own how she deals with the fact. The benefit is that the preview does not stop at the ‘unknown apply’ anymore, and permits to give more details on what follows this apply.