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

Suggesion: Define Operators with functions #56

Open
Haringat opened this issue May 30, 2022 · 11 comments
Open

Suggesion: Define Operators with functions #56

Haringat opened this issue May 30, 2022 · 11 comments

Comments

@Haringat
Copy link

Haringat commented May 30, 2022

Analogous to Object.defineProperty/Object.defineProperties we could have Object.defineOperator/Object.defineOperators to define operators for objects/prototypes. This would be in line with current designs for defining things for objects. Like with Object.defineProperty trying to re-define a previously defined operator for an object should throw.

Typescript definition

interface Operators<TFirst> {
    "+"(first: TFirst, second: unknown): TFirst;
    "-"(first: TFirst, second: unknown): TFirst;
    "*"(first: TFirst, second: unknown): TFirst;
    "/"(first: TFirst, second: unknown): TFirst;
    "~"(first: TFirst): TFirst;
    "++"(first: TFirst): TFirst;
    "--"(first: TFirst): TFirst;
    // TODO: add all overloadable operators
}

interface ObjectConstructor {
    defineOperator<TObject extends object, TOperator extends keyof Operators<TObject>>(o: TObject, operator: TOperator, operatorDefinition: Operators<TObject>[TOperator]): TObject;
    defineOperators<TObject extends object>(o: TObject, operatorDefinitions: Partial<Operators<TObject>>): TObject;
}

Examples

const o = {
    counter: 1
};

Object.defineOperator(o, "+", (a, b) => {
    return {
        counter: a.counter + b
    };
});

console.log(o.counter); // 1
o = o + 3;
console.log(o.counter); // 4
Object.defineOperator(o, "+" (a, b) => {
    return {
    };
}); // TypeError: Cannot redefine operator: +
class Foo {
    p = 1;
    constructor(p) {
        this.p = p;
    }
}

Object.defineOperator(Foo.prototype, "++", (a) => {
    a.p++;
    return a;
});

class Bar extends Foo {
    o = 1;
}

Object.defineOperator(Bar.prototype, "++", (a) => {
    a.p++;
    a.o++;
    return a;
});

Design goals

  • Expressivity:
    • Support operator overloading on both mutable and immutable objects, and in the future, typed objects and value types. (would work)
    • Support operands of different types and the same type, as in the above examples. (would work, in TypeScript the second operator would have to be assumed to be either any or unknown)
    • Explain all of JS's behavior on existing types in terms of operator overloading. (I am unsure what the author meant with that)
    • Available in both strict and sloppy mode, with and without class syntax. (would work, perhaps some syntax sugar for classes could be invented)
  • Predictability
    • The meaning of operators on existing objects shouldn't be overridable or monkey-patchable, both for built-in types and for objects defined in other libraries. (They would be overridable in class hierarchy, but not monkey-patchable on the same object and not definable for primitive values)
    • It should not be possible to change the behavior of existing code using operators by unexpectedly passing it an object which overloads operators. (If this is feasible.) (This goal would be violated, however very unlikely since code written without operator overloading in mind would not use operators on objects and thus would not change behavior. Changing the behavior of overloaded operators of 3rd party code is impossible, as the different definitions would create an error.)
    • Don't encourage a crazy coding style in the ecosystem. (I would never do that :) )
  • Efficiently implementable
    • In native implementations, don't slow down code which doesn't take advantage of operator overloading (including within a module that uses operator overloading in some other paths). (Would be violated because the implementation would need to check for the presence of the internal slot on the object. In practice that would not make much difference as operators would probably only be used on objects on which they are overloaded since the current language-definition for the result of using operators on objects does not yield useful results for most cases)
    • When operator overloading is used, it should lend itself to relatively efficient native implementations, including - In the startup path, when code is run just a few times - Lends itself well to inline caching (for both monomorphic and polymorphic cases) to reduce any overhead of the dispatch - Feasible to optimize in a JIT (for both monomorphic and polymorphic cases), with a minimal number of cheap hidden class checks, and without extremely complicated cases for when things become invalid - Don't create too much complexity in the implementation to support such performance
    • When enough type declarations are present, it should be feasible to implement efficiently in TypeScript, similarly to BigInt's implementation. (see definitions above)
  • Operator overloading should be a way of 'explaining the language' and providing hooks into something that's already there, rather than adding something which is a very different pattern from built-in operator definitions. (would be followed)
@ljharb
Copy link
Member

ljharb commented May 30, 2022

I’m pretty sure this would slow down all code using any overloadable operators, because the engine would have to check if the operands had operators defined.

@Haringat
Copy link
Author

@ljharb I doubt that für three reasons:

  1. The whole thing only applies to objects so operators on primitives would work the same as before.
  2. It only has to be checked when an operator is used on an object. I am yet to come across any serious code where an operator is used on an object wanting it to be turned into "[object Object]". So when an operator will be used on an object it will most certainly be to leverage operator overloading.
  3. Checking the presence of an internal slot on an object should not take THAT long. Sure, it might be considerable when interpreting but when AOT or JIT compiling that should be taken care of and it should directly fall through to either using the overloaded operator of the fallback (current) behavior, unless the code is absolutely written to not be optimizable but then I would (at least partially) blame it on the author of the code because any tool can only help you if you use it correctly. That is especially true for optimization tools.

@ljharb
Copy link
Member

ljharb commented May 30, 2022

Serious code does that with Date objects all the time - lots of existing things use valueOf/toString/Symbol.toPrimitive to “overload” objects.

@Haringat
Copy link
Author

@ljharb At this point we would really need some data. Sadly, I lack the resources to scrape GitHub or npm scanning how often that is actually used. My gut still says that it will not be too much (at least if you neglect built-in classes like Date for which operator overloading could be prohibited).

@ljharb
Copy link
Member

ljharb commented May 31, 2022

Date means that all objects must always be checked, on many websites - github/npm data won’t change that.

@hax
Copy link
Member

hax commented May 31, 2022

I don't get it, consider current date - 1000 actually call date.toPrimitive("number") - 1000, what's the performance differences if it call getOperator(date, "-")(date, 1000)?

@FrameMuse
Copy link

I don't get it, consider current date - 1000 actually call date.toPrimitive("number") - 1000, what's the performance differences if it call getOperator(date, "-")(date, 1000)?

I think the goal is not in perfomance but in the fact that it will return your Date object back with resulting time and not just number as in your case.

date - 1000 will return something like 1654447791733 (Status quo)
date - 1000 will return something like new Date(1654447791733) or whatever output you want (Operator overloading)

@Haringat
Copy link
Author

@FrameMuse I think once again operator overloading comes down to what it always comes down to: Only use it if there is exactly one logical implementation.
As for the date/toPrimitive thing: I find it rather unlikely that someone would really overload that as it would obviously break stuff but to be sure it would probably be possible to built-in an overload for operators on the Date class since that would block any further overloads.

@hax
Copy link
Member

hax commented Jun 22, 2022

I think the goal is not in perfomance

@FrameMuse I think you misunderstand my question, I asked that question because of #56 (comment) .

@FrameMuse
Copy link

@hax Yes, I did, sorry. Thank you for pointing it out.

@Haringat
Copy link
Author

date - 1000 will return something like 1654447791733 (Status quo)
date - 1000 will return something like new Date(1654447791733) or whatever output you want (Operator overloading)

That depends on the implementation of the operator.
You could probably solve that without breaking current code by changing the rules as follows:

  1. look if the object has toPrimitive
  2. if it does, transform the object into a primitive and use current logic.
  3. if it does not, look for overloaded operators.

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

4 participants