Open pie-flavor opened 2 months ago
This particular example is still confusing me:
function example() {
return {
[Symbol.result]() {
return [new Error("123"), null]
},
}
}
const [error, result] ?= example() // Function.prototype also implements Symbol.result
// const [error, result] = example[Symbol.result]()
// error is Error('123')
This implies it is evaluated as: example.@@result()
but the error comes from the object returned from the function. It is also mentioned that Function has a Symbol.result
but how that actually works? The handler in Function's prototype still needs to execute the actual function, get the return value and return it.
How will the function instance passed on to the handler on the prototype? Is it an implied mechanism similar to this
being set on function calls directly on a class's instance?
class A {
internalValue = 3;
myMethod() {
return this.internalValue;
}
}
const a = new A();
a.myMethod(); // works as expected - "this" is set to "a"
const b = a.myMethod;
b(); // doesn't have a reference to the instance
What is the equivalent mechanism for the result symbol? It should be possible to implement a custom version of this mechanism, be it for poly-filling reasons or other.
Edit: the class example is maybe somewhat irrelevant, there is no way we loose track of the function for the new operator, but I am asking how it is passed in to the result handler? Maybe this
is how it should be handled?
Ok, I think it is obviously based on this
again (also see the polyfill provided in the repo), I just missed it previously. See this implementation:
const result = Symbol("result")
function example() {
return {
[result]() {
return [new Error("123"), null]
}
}
}
Function.prototype[result] = function() {
return [this()[result](), null];
}
const [value, error] = example[result]();
console.log(value, error);
For the object version:
const result = Symbol("result")
const example = {
[result]() {
return [new Error("123"), null]
}
};
Object.prototype[result] = function() {
return [this[result](), null];
}
const [value, error] = example[result]();
console.log(value, error);
Notice the difference between the two implementations - we are calling it if it is a function and not if it is an object. That special casing is the reason why I agree with @pie-flavor.
This spec describes that
[err, res] ?= func(x)
is evaluated likefunc.@@result(x)
. However, it also describes that[err, res] ?= obj
is evaluated likeobj.@@result()
. This would imply that[err, res] ?= (func(x))
is evaluated likefunc(x).@@result()
. One can easily imagine a code formatting style, preprocessor, or bulk-editing oversight that creates a case like that.Specifying that non-existing
@@result
producesTypeError
would protect against this case, except that becausePromises
are handled via a general rule of recursion, no such error would be thrown for a Promise-returning function (or other@@result
-implementing value).This can produce a line of code that the author expects will never throw, but can throw.
More generally, this syntax breaks other expectations about general programming, such as that evaling an expr is no different from assigning it to a variable and then evaling the variable. An example of a function call being treated specially would be Go's
defer f(x)
evaluatingx
immediately instead of at the time of invocation, but Go counterbalances this by forbidding any other kind of expression so if you screw it up you get an immediate error.In my opinion, if a function call expr is treated specially, all other types of expr should be forbidden. An element of syntax can be an alternative way to call a function, or an operator on the result of an expression, but should never be both. (This implies processing
await foo(x)
directly as a second case, instead of through recursion.)