Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Special-casing particular expression types is a footgun #12

Open
pie-flavor opened this issue Aug 16, 2024 · 3 comments
Open

Special-casing particular expression types is a footgun #12

pie-flavor opened this issue Aug 16, 2024 · 3 comments

Comments

@pie-flavor
Copy link

pie-flavor commented Aug 16, 2024

This spec describes that [err, res] ?= func(x) is evaluated like func.@@result(x). However, it also describes that [err, res] ?= obj is evaluated like obj.@@result(). This would imply that [err, res] ?= (func(x)) is evaluated like func(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 produces TypeError would protect against this case, except that because Promises 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) evaluating x 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.)

@anacierdem
Copy link

anacierdem commented Aug 16, 2024

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?

@anacierdem
Copy link

anacierdem commented Aug 16, 2024

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.

@arthurfiorette
Copy link
Owner

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants