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

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

Open
sirisian opened this issue May 16, 2020 · 7 comments

Comments

@sirisian
Copy link

sirisian commented May 16, 2020

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
Copy link
Member

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
Copy link

trusktr commented Jun 5, 2020

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
Copy link

trusktr commented Jun 5, 2020

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
Copy link
Member

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
Copy link

b-strauss commented Jun 8, 2020

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
Copy link

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
Copy link

InSyncWithFoo commented Jul 14, 2023

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?'

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

6 participants