Open getify opened 4 weeks ago
Sure! this was my first approach when trying to polyfill the idea into actual JS. I'm also afraid that we change to try
syntax instead of ?=
, which, following the https://tc39.es/proposal-explicit-resource-management precedent of using
and await using
, we will probably end up with try
and try await
which could use two different symbols.
Something like Symbol.result
and Symbol.asyncResult
, just like Symbol.dispose
and Symbol.asyncDispose
.
?*=
or try *
feels pretty intuitive, I like that idea @getify
Also implementing the recursive part could easily be a later standard which would hopefully get this accepted and over the line sooner?
Recursive unwrapping makes this feature non-composable and not usable in a generic context. Consider looking up a user in a database. I may have a DbAccessError
if the database is down, or a AuthError
if the user has blocked me, or I may receive a user. I would express this as Result<Result<User, AuthError>, DbAccessError>
. Now, consider a generic function that handles the db access error:
function handleDbAccessError(doDatabaseAction: () => Result<T, DbAccessError>) {
const [err, value] = doDatabaseAction(primaryDatabase);
if (err != null) {
// is err a DbAccessError? NO! It's actually **anything** because of the recursive handling
alert('Sadness, database is not accessible: ' + err.accessException.toString()) // oops this throws, because
// auth error doesn't have .accessException
}
}
(Re: @JAForbes's point. This illustrates why we could never add recursive handling afterward.)
@rbalicki2
Either changing from recursive to non-recursive, or vice versa, would certainly be an intolerable hard breaking change. However, I think @JAForbes meant that we could add one of these later, as an additional capability, not that we'd make a breaking change.
Ah, understood. Thank you for the clarification!
As I understand it, the motivation for recursive unwrapping is to make ?= await getPromise()
work. But even there, it does so at the wrong level. Consider:
async function getPromise() {
return 1;
}
const [error, data] ?= await getPromise();
console.log(data); // 1
const [error, promise] ?= getPromise();
const data = await promise; // plain old = here
console.log(data); // [null, 1]
By separating the function call from the await
and only using ?=
with the former, I’ve clearly indicated that I expect to process errors only from the former—yet the recursive unwrapping has already happened and mangled the promise to process errors from the latter.
The proposal seems focused on promises, but I don't understand how it can work with functions that return functions (any kind of function). As written, it seems like you can't call a function that returns a function without the returned function being called automatically (and so on until there's a result that's not a function). That would be surprising and unwanted behaviour.
I agree with this and it was only my first idea around how to support async functions
. I guess in the end this will also be solved if we migrate to this syntax
I'm aware that the recursive unwrapping is a convenience, and mirrors the same behavior in promises. However, I would like to urge its reconsideration -- or at the very least, register the objection here in an issue, for posterity sake.
Back Story: Promises
The fact that I cannot carry a Promise inside another Promise (
Promise<Promise>
) creates an overfit in locality of result handling. By that I mean, if Iawait
orthen()
a promise, I'm forced to deal with the ultimate result of that at that point in the code. I cannot hold multiple layers of result wrapped together, and progressively unwrap and handle those layers one at a time across a call-stack.To put this in other terms, the recursive unwrapping (or refusal to even nest!) design aspect of promises is what makes
Promise
not compatible with the monadic laws -- despite all the well-intentioned blog posts that claim JS promise is a monad. This design choice is what breaks down a variety of techniques that are established around those guarantees (in the world of FP programming).Undoubtedly, convenience is a strong driver for that design. Probably also performance.
But I think the case could/should still have been considered that, it would have been preferable if there was an option to nest Promises, and to unwrap them one level at a time, even if the default behavior might have been to recursively flatten them out.
Here We Go Again
So now we arrive at the present
?=
proposal, and the implied[error, data]
tuple type. If we handwave a bit, this type is basically an Either monad. Which is cool.The explainer illustrates we can manually construct nested tuples, just like you could do with real Either instances. That's cool, and an improvement over the Promise type which didn't allow the nesting at all (it basically unwraps/flattens at
resolve()
time). I assume that if the tuple in question were an actual concrete value type of the language (asPromise
is), the design would probably prevent such nesting.Since we can nest these values, we're closer to being able to take advantage of the relevant monadic guarantees and design patterns. But then the
?=
recursive unwrapping kicks the legs out from underneath us. If we use?=
, we lose the monadic'ness. We can make Either piggyback on the tuple type's design, but have to use custom userland functions for the handling, instead of the code readability and attractiveness of?=
operator itself. That's disappointing.Reconsider?
I'm not asking to discard the recursive unwrapping entirely, but rather, could we possibly have both options available, one where the recursive unwrapping is done (for those who prefer the convenience), and one where the unwrap is not recursive?
I could bikeshed here on
?=
vs?*=
as a pair of operators for this purpose. But before that's relevant to discuss, I just wanted to raise the question if we could reconsider this design decision before it's too far baked?