Skip to content
This repository was archived by the owner on Apr 3, 2025. It is now read-only.
This repository was archived by the owner on Apr 3, 2025. It is now read-only.

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

Open
@sirisian

Description

@sirisian

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions