tc39 / proposal-operator-overloading

MIT License
619 stars 20 forks source link

Operator overloading class syntax using TS-like method overloading and "extension" classes #29

Open sirisian opened 4 years ago

sirisian commented 4 years ago

This would keep all the current static behavior with the goal of using a more future-proof (for types) syntax choice.

Syntax for each operator:

class A {
  operator+=(rhs:Type) {}
  operator-=(rhs:Type) {}
  operator*=(rhs:Type) {}
  operator/=(rhs:Type) {}
  operator%=(rhs:Type) {}
  operator**=(rhs:Type) {}
  operator<<=(rhs:Type) {}
  operator>>=(rhs:Type) {}
  operator>>>=(rhs:Type) {}
  operator&=(rhs:Type) {}
  operator^=(rhs:Type) {}
  operator|=(rhs:Type) {}
  operator+(rhs:Type) {}
  operator-(rhs:Type) {}
  operator*(rhs:Type) {}
  operator/(rhs:Type) {}
  operator%(rhs:Type) {}
  operator**(rhs:Type) {}
  operator<<(rhs:Type) {}
  operator>>(rhs:Type) {}
  operator>>>(rhs:Type) {}
  operator&(rhs:Type) {}
  operator|(rhs:Type) {}
  operator^(rhs:Type) {}
  operator~() {}
  operator==(rhs:Type) {}
  operator!=(rhs:Type) {}
  operator<(rhs:Type) {}
  operator<=(rhs:Type) {}
  operator>(rhs:Type) {}
  operator>=(rhs:Type) {}
  operator&&(rhs:Type) {}
  operator||(rhs:Type) {}
  operator!() {}
  operator++() {} // prefix (++a)
  operator++(nothing) {} // postfix (a++)
  operator--() {} // prefix (--a)
  operator--(nothing) {} // postfix (a--)
  operator-() {}
  operator+() {}
}

This would use a fake method overloading syntax to specialize for each right hand side type.

Ad-hoc examples:

class Vector2 extends Float32Array {
  constructor(...v) {
    super(2);
    this.set(v);
  }
  get x() {
    return this[0];
  }
  set x(x) {
    this[0] = x;
  }
  get y() {
    return this[1];
  }
  set y(y) {
    this[1] = y;
  }
  operator+(v:Vector2) {
    return new Vector(this.x + v.x, this.y + v.y);
  }
  operator+=(v:Vector2) {
    this.x += v.x;
    this.y += v.y;
  }
  operator-(v:Vector2) {
    return new Vector(this.x - v.x, this.y - v.y);
  }
  operator-=(v:Vector2) {
    this.x -= v.x;
    this.y -= v.y;
  }
  operator*(s:Number) {
    return new Vector(this.x * s, this.y * s);
  }
  operator*=(s:Number) {
    this.x *= s;
    this.y *= s;
  }
  operator*(v:Vector2) {
    return new Vector2(this.x * v.x, this.y * v.y);
  }
  operator*=(v:Vector2) {
    this.x *= v.x;
    this.y *= v.y;
  }
  operator/(s:Number) {
    return new Vector(this.x * s, this.y * s);
  }
  operator/=(s:Number) {
    this.x /= s;
    this.y /= s;
  }
  operator/(v:Vector2) {
    return new Vector2(this.x / v.x, this.y / v.y);
  }
  operator/=(v:Vector2) {
    this.x /= v.x;
    this.y /= v.y;
  }
  operator-() {
    return new Vector2(-this.x, -this.y);
  }
  operator==(v:Vector2) {
    return Math.abs(this.x - v.x) < 0.0001 && Math.abs(this.y - v.y) < 0.0001;
  }
  length() {
    return Math.hypot(this.x, this.y);
  }
  lengthSquared() {
    return this.x**2 + this.y**2;
  }
  normalize() {
    const length = this.Length();
    if (length != 0) {
      this /= length;
    }
    return this;
  }
  project(v) {
    return v * Vector2.dot(this, v) / v.lengthSquared();
  }
  set(x, y) {
    this.x = x;
    this.y = y;
    return this;
  }
  clone() {
    return new Vector2(this.x, this.y);
  }
  static dot(v1, v2) {
    return v1.x * v2.x + v1.y * v2.y;
  }
  static cross(v1, v2) {
    return v1.x * v2.y - v1.y * v2.x;
  }
  static distance(v1, v2) {
    return Math.hypot(v2.x - v1.x, v2.y - v1.y);
  }
  static distanceSquared(v1, v2) {
    return (v2.x - v1.x)**2 + (v2.y - v1.y)**2;
  }
}
class Matrix4x4 extends Float32Array {
  /*
  m11, m12, m13, m14,
  m21, m22, m23, m24,
  m31, m32, m33, m34,
  m41, m42, m43, m44
  */
  constructor(...m) {
    super(16);
    this.set(m);
  }
  operator+(m:Matrix4x4) {
    return new Matrix4x4(...this.map((value, index) => value + m[index]));
  }
  operator+=(m:Matrix4x4) {
    for (let i = 0; i < this.length; ++i) {
      this[i] += m[i];
    }
  }
  operator-(m:Matrix4x4) {
    return new Matrix4x4(...this.map((value, index) => value - m[index]));
  }
  operator-=(m:Matrix4x4) {
    for (let i = 0; i < this.length; ++i) {
      this[i] -= m[i];
    }
  }
  operator*=(m:Matrix4x4) {
    return new Matrix4x4(
      this[0] * m[0] + this[1] * m[4] + this[2] * m[8] + this[3] * m[12],
      this[0] * m[1] + this[1] * m[5] + this[2] * m[9] + this[3] * m[13],
      this[0] * m[2] + this[1] * m[6] + this[2] * m[10] + this[3] * m[14],
      this[0] * m[3] + this[1] * m[7] + this[2] * m[11] + this[3] * m[15],
      this[4] * m[0] + this[5] * m[4] + this[6] * m[8] + this[7] * m[12],
      this[4] * m[1] + this[5] * m[5] + this[6] * m[9] + this[7] * m[13],
      this[4] * m[2] + this[5] * m[6] + this[6] * m[10] + this[7] * m[14],
      this[4] * m[3] + this[5] * m[7] + this[6] * m[11] + this[7] * m[15],
      this[8] * m[0] + this[9] * m[4] + this[10] * m[8] + this[11] * m[12],
      this[8] * m[1] + this[9] * m[5] + this[10] * m[9] + this[11] * m[13],
      this[8] * m[2] + this[9] * m[6] + this[10] * m[10] + this[11] * m[14],
      this[8] * m[3] + this[9] * m[7] + this[10] * m[11] + this[11] * m[15],
      this[12] * m[0] + this[13] * m[4] + this[14] * m[8] + this[15] * m[12],
      this[12] * m[1] + this[13] * m[5] + this[14] * m[9] + this[15] * m[13],
      this[12] * m[2] + this[13] * m[6] + this[14] * m[10] + this[15] * m[14],
      this[12] * m[3] + this[13] * m[7] + this[14] * m[11] + this[15] * m[15]);
  }
  clone() {
    return new Matrix4x4(...this);
  }
  static Identity() {
    return new Matrix4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
  }
  static Scaling(s) {
    return new Matrix4x4(s, 0, 0, 0, 0, s, 0, 0, 0, 0, s, 0, 0, 0, 0, 1);
  }
  static RotationX(a) {
    const cos = Math.cos(a);
    const sin = Math.sin(a);
    return new Matrix4x4(1, 0, 0, 0, 0, cos, -sin, 0, 0, sin, cos, 0, 0, 0, 0, 1);
  }
  static RotationY(a) {
    const cos = Math.cos(a);
    const sin = Math.sin(a);
    return new Matrix4x4(cos, 0, sin, 0, 0, 1, 0, 0, -sin, 0, cos, 0, 0, 0, 0, 1);
  }
  static RotationZ(a) {
    const cos = Math.cos(a);
    const sin = Math.sin(a);
    return new Matrix4x4(cos, -sin, 0, 0, sin, cos, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
  }
  static Translation(v) {
    return new Matrix4x4(1, 0, 0, v.x, 0, 1, 0, v.y, 0, 0, 1, v.z, 0, 0, 0, 1);
  }
}

The second new feature would be the ability to define extension classes. Currently declaring a class with the same name causes a redeclaration SyntaxError. I'd propose that if the class only has, for now, operators that it's treated as an extension and would simply act like a partial class merging into any previously declared class as long as there are no signature conflicts in the operator overloads.

// Extension operators for Number
class Matrix4x4 {
  operator*(s:Number) {
    return new Matrix4x4(...this.map((value, index) => value * s));
  }
  operator*=(s:Number) {
    for (let i = 0; i < this.length; ++i) {
      this[i] *= m[i];
    }
  }
  operator/(s:Number) {
    return this * (1 / s);
  }
  operator/=(s:Number) {
    this *= 1 / s;
  }
}

// Extension operators for Vector4
class Matrix4x4 {
  operator+(v:Vector4) {
    return new Matrix4x4(...this) +=
  }
  operator+=(v:Vector4) {
    this[3] += v.x;
    this[7] += v.y;
    this[11] += v.z;
  }
  operator*(v:Vector4) {
    return new Vector4(
      this[0] * v.x + this[1] * v.y + this[2] * v.z + this[3] * v.w,
      this[4] * v.x + this[5] * v.y + this[6] * v.z + this[7] * v.w,
      this[8] * v.x + this[9] * v.y + this[10] * v.z + this[11] * v.w,
      this[12] * v.x + this[13] * v.y + this[14] * v.z + this[15] * v.w);
  }
}

// Extension operators for Matrix3x3
class Matrix4x4 {
  operator*=(m:Matrix3x3) {
    return new Matrix4x4(
      this[0] * m[0] + this[1] * m[3] + this[2] * m[6],
      this[0] * m[1] + this[1] * m[4] + this[2] * m[7],
      this[0] * m[2] + this[1] * m[5] + this[2] * m[8],
      this[3],
      this[4] * m[0] + this[5] * m[3] + this[6] * m[6],
      this[4] * m[1] + this[5] * m[4] + this[6] * m[7],
      this[4] * m[2] + this[5] * m[5] + this[6] * m[8],
      this[7],
      this[8] * m[0] + this[9] * m[3] + this[10] * m[6],
      this[8] * m[1] + this[9] * m[4] + this[10] * m[7],
      this[8] * m[2] + this[9] * m[5] + this[10] * m[8],
      this[11],
      this[12] * m[0] + this[13] * m[3] + this[14] * m[6],
      this[12] * m[1] + this[13] * m[4] + this[14] * m[7],
      this[12] * m[2] + this[13] * m[5] + this[14] * m[8],
      this[15]);
  }
}

There is a caveat here with the extension syntax. It's elegant for user made classes and extending them in compartmentalized ways, but this syntax doesn't work for intrinsic objects. Things like Boolean, Number, BigInt, String, etc. You can't just write:

// Extension operators for Vector2
class Number {
  operator*(v:Vector2) {
    return v * s;
  }
}

// Extension operators for Matrix4x4
class Number {
  operator*(v:Matrix4x4) {
    return v * s;
  }
}

You'd need to use the function syntax, like the spec proposal has, to declare them. Maybe I'm thinking about this part wrong, but it makes my suggestion awkward as ideally you'd want a single elegant syntax throughout a codebase without having to use two separate systems. If there was a way to refer to the intrinsic objects and use an extension syntax that would be awesome, but it's not clear to me if that's possible. (In an elegant and consistent way within JS).

littledan commented 4 years ago

I like the idea of making more ergonomic syntax for classes with operator overloading. Do you have an idea of the algorithm you'd use for determining which overload to select? Do you like the one described in this proposal, or do you prefer another one?

trusktr commented 4 years ago

What about decorators? Could a built-in decorator be used, similar to what AssemblyScript does?

class Vector {
  @operator("*")
  static mul(left: Vector, right: Vector): Vector { ... }

  @operator("*")
  mul(right: Vector): Vector  { ... }
}

As a side-effect, you can also use the method directly:

vec.mul(otherVec)
trusktr commented 4 years ago

I too dislike that we need to make two parallel class hierarchies in order to achieve operator overloading with the current proposal. It would be much more ergonomic to define a single Vector3 class (for example) with or without it having operator overloads (instead of one class boxing another class).

littledan commented 4 years ago

I proposed decorators in a previous draft that you can see in the git log, but removed them due to both negative feedback about the syntax and uncertainty about how decorators will go exactly.

b-strauss commented 4 years ago

The second new feature would be the ability to define extension classes. Currently declaring a class with the same name causes a redeclaration SyntaxError. I'd propose that if the class only has, for now, operators that it's treated as an extension and would simply act like a partial class merging into any previously declared class as long as there are no signature conflicts in the operator overloads.

I would propose to extend mixins with operators for that usecase.

https://github.com/justinfagnani/proposal-mixins

Example using parts of your code:

mixin Matrix4x4Mixin {
  constructor(...m) {
    super(16);
    this.set(m);
  }

  operator+(m) {
    return new this.constructor(...this.map((value, index) => value + m[index]));
  }

  clone() {
    return new this.constructor(...this);
  }

  static Identity() {
    return new this(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
  }
}

class Matrix4x4 extends Array with Matrix4x4Mixin {
}

class Matrix4x4Int8 extends Int8Array with Matrix4x4Mixin {
}
zweimach commented 4 years ago

Instead of using operator methods, isn't it more ergonomic to use Symbol?

Instead of

class Point {
  operator+(rhs) {}
  operator==(rhs) {}
  operator!=(rhs) {}
}

We could use

class Point {
  [Symbol.operator["+"]](rhs) {}
  [Symbol.operator["=="]](rhs) {}
  [Symbol.operator["!="]](rhs) {}
}
InSyncWithFoo commented 1 year ago

We could use

class Point {
  [Symbol.operator["+"]](rhs) {}
  [Symbol.operator["=="]](rhs) {}
  [Symbol.operator["!="]](rhs) {}
}

I like this one the most out of all suggestions I have read. The only problem with this approach is that unary + (as in +foo) and binary + (as in foo + bar) need to be differentiated somehow.

Perhaps we can over-specify things a bit, like this?

class Point {
  [Symbol.operator.unary['+']]() {}
  [Symbol.operator.unary['++']]() {}

  [Symbol.operator.binary['+']](rightOperand) {}
  [Symbol.operator.binary['??']](rightOperand) {}

  [Symbol.operator.assignment['+=']](value) {}
  // or Symbol.operator.assignment['+'](value) {}

  [Symbol.operator.comparison['==']](other) {}

  /*** And probably (or probably not)... ***/

  // Interchangeable with handler.deleteProperty()
  [Symbol.operator.keywords['delete']](property) {}

  // Interchangeable with handler.has()
  [Symbol.operator.keywords['in']](property) {}

  // Interchangeable with handler.construct()
  [Symbol.operator.keywords['new']](property) {}

  // await seeking for equity.
  [Symbol.operator.keywords['await']](property) {}
}

Some already has their own well-known Symbols: instanceof (Symbol.hasInstance), for (... of ...) (Symbol.iterator), for await (... of ...) (Symbol.asyncIterator), etc. Some others with (in my opinion) concrete meanings might not even be overloadable: ?., await, yield, typeof, void, etc.

Not to mention, Symbol.toPrimitive has already provided a way to overload coercion:

class C {
  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number':
        return 42;
      case 'string':
        return 'Hello';
      default:
      // or case 'default':
        return null;
    }
  }
};

console.log(+new C()); // 42
console.log(`${new C()} world!`); // 'Hello world!'
console.log(new C() + 'ish?'); // 'nullish?'