tc39 / proposal-operator-overloading

MIT License
622 stars 20 forks source link

[IDEA] operator overloading via Proxy traps #54

Open Haringat opened 2 years ago

Haringat commented 2 years ago

There is already the possibility to trap certain operators on objects and similar (e.g. () or new) by using Proxy traps. For consistency it would seem sensible to put the overloading of other operators there as well. It could work like this:


class Vector {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
}

Vector.prototype = new Proxy(Vector.prototype, {
    add(target, other) {
        if (typeof other === "number") {
            return new Vector(
                target.x + other,
                target.y + other
            );
        } else if (typeof other === "object" && typeof other.x === "number" && typeof other.y === "number") { // TODO: add case for bigint?
            return new Vector(
                target.x + other.x,
                target.y + other.y
            );
        } else {
            throw new Error("unsupported operand provided for addition of vector");
        }
    }
    // TODO: add further operators
});

const one = new Vector(1, 1);
const two = one + one;
console.log(two); // Vector { x: 2, y: 2 }
const three = one + 2;
console.log(three); // Vector { x: 3, y: 3 }
const bad = 3 + one; // coercion appearing as number has no overload (and cannot have one) for objects
console.log(bad); // 3[object Object]

The "sugared" version is to be defined.

This would of course bring some further changes to the proposal, most notably that the overloaded operators would be inherited and always be present as there is no "with" to explicitly enable them. However, that would probably not be a problem as it is in line with how Proxies (and thus our currently limited operator overloading) works.

jhmaster2000 commented 1 year ago

This sounds like a nice idea on paper to follow existing practices but in reality this falls apart really bad with a number of flaws:

  1. The shown example is impossible to begin with because class locks the .prototype property to read-only

    Uncaught TypeError: Cannot assign to read only property 'prototype' of function 'class Vector {...}'

  2. "Ok, let's just change that so class doesn't make the prototype readonly!" That would be a breaking change, so realistically its not going to happen.

  3. "Ok, let's just de-sugar the class down to its ES5 function version" Alright, here it is:

    function Vector(x, y) {
    if (!new.target) throw new TypeError(`Class constructor ${this.name} cannot be invoked without 'new'`);
    this.x = x;
    this.y = y;
    }
    Vector.prototype = new Proxy(Vector.prototype, {});

    The code doesn't error anymore, great, but we still can't make class de-sugar to this, without locking the prototype, because it would be a breaking change, so we would basically be forcing people to use ES5 function "classes" syntax if they wanted to use operator overloading, and nobody wants to write these ugly ES5 functions in 2023.

  4. "Ok, lets make it so it only desugars to unlocked prototype if it contains operator overloads" Indeed this wouldn't be a breaking change, but it would add unpredictability and inconsistent behavior based on how you write your classes which would just cause more harm than good overall, not a good solution.

Let's ignore for a second all the problems with even getting it to work and assume we do get it to work somehow without violating spec or breaking changes. Plenty of problems remain...

  1. Proxies are infamous for their abysmal performance, even a completely empty proxy with no traps is over 90% slower to just initialize, 95% slower at simple property access and over 99% slower at simple property setting, test for yourself:

And of all things, we'd be proxying a prototype, an object that is highly accessed and checked against from all sorts of places in the language semantics like inheritance, constructors, any kind of property access that doesn't match an own property, etc. It'd be a total widespread performance disaster.

  1. Okay so, proxies are slow, but why? Can't we make them faster? I can't say much about this one in exact detail since I'm not a JS engine developer so I don't know the bare and raw truths of it, but conceptually, the problem arises from the fact proxies are not statically analyzable and completely unpredictable, so engines have absolutely no way of optimizing anything about them, or even worse, any code surrounding them at all! Since it is the very nature of proxies to intercept and inject brand new behavior into existing semantics, they are fundamentally incompatible with any form of static analysis and its not possible to make them statically analyzable without taking away their very purpose, so no, there isn't much that can be done to optimize them sadly.

  2. Ok but if we make syntax sugar for operators that is statically analyzable, it should work right? Like classes? Well no, because it doesn't matter if you can statically analyze something but not actually put in practice the optimizations you know you could do based on your analysis, which you won't be able to do when you down-level to the desugared version with the Proxy. Classes only work because they desugar to code that implements the restrictions they impose to be statically analyzable, such as the locking of the prototype and check against calls without new.

Even if we pretend further, that we somehow find a way for proxies to not have such absurd performance drains, we still have many more smaller but not insignificant issues left... simply too many to continue on listing here, I hope what I've listed so far has been enough to make anyone see how terrible of an idea it would be to proxy prototypes.

In an ending note, I do share the wishful intent here for integrating with existing language semantics rather than coming up with brand new extra things to memorize, proxies are just the wrong call, instead, how about we consider well-known symbols or decorators instead? Although personally I'm also pretty fond of the primary syntax suggested in the linked issue itself despite being new syntax.