Operator Overloading
Yep, we're talking about it
Why have operator overloading at all?
- Useful for new proposed data types - BigInt, Decimal
- Operator support make these "look more like" first class data types
- Offers a set of well-understood base level semantics (`+` is better understood than `.plus`)
- C++, F#, Scala, Ruby, Haskell, & Rust all agree!
Why have operator overloading at all?
const a = 2n
const b = 8n
const c = 6n
a + b - c == 4n // Looks like real JS
a.plus(b.minus(c)).equals(4n) // Looks like a hot mess.
But why give userland operator overloading?
- Allows community to define what would be useful in std lib (Date, Vector, Point, Coords, Color)
- Userland can polyfill newer data types
- We might be able to make `==` useful again!
This is a dark path...
- Could be slow in runtime (we already have `Proxy`, `Symbol.toPrimitive`)
- May give Gary Bernhardt enough fuel for his next Wat talk
- Could lead to "operator overloading overload"
This is a dark path...
Number.prototype['operator+'] = () => {
throw new Error('No addition for you!')
}
Boolean.prototype['operator=='] = function () {
return true // false is now true!
}
...that's already possible...
Number.prototype.toString = () => {
throw new Error('No toString for you!')
}
10..toString() // No toString for you!
...and good practices always prevail
- "Never modify prototypes"
- "Always use triple eq"
- "Only use operator overloading for good"
Requirements Gathering
- Support multiple types
- Handle operator ordering
- Be intuitive to JS
Support multiple types
class Celsuis(c) { ... }
class Fahrenheit(f) { ... }
new Celsuis(60) > new Fahrenheit(60) === true
class Point(x, y) { ... }
class Vector(...points) { ... }
Point(1, 1) + Point(1, 0) == Vector([1, 1], [1, 0])
Handle operator ordering
class Celsuis(c) { ... }
class Fahrenheit(f) { ... }
const c = new Celcius(60)
const f = new Fahrenheit(60)
c > f // c.greaterThan(f) or f.greaterThan(c)?
f > c // What about now?
f > [] // What about now?
Handle operator ordering - Double Dispatch
class Celsuis(c) { ... }
class Fahrenheit(f) { ... }
const c = new Celcius(60)
const f = new Fahrenheit(60)
c > f // c.greaterThan(f) && f.greaterThan(c)
Handle operator ordering - Double Dispatch
class Point(x, y) { ... }
const p = new Point(1, 1)
const a = [1, 0]
const v = p + a // Calls p.plus(a) and ... a.plus(p)?
v // What is this value now?
Handle operator ordering - SameValue
function addPointOrVector(a, b) { ... }
class Point(x, y) { add: addPointOrVector }
class Vector(...points) { add: addPointOrVector }
const p = new Point(1, 1)
const a = [1, 0]
let v
v = p + a // Usual semantics: `NaN`
v = a + p // Usual semantics: `NaN`
v = p + p // addPointOrVector(p, p)
v = new Vector() + p // addPointOrVector(new Vector(), p)
v // v is easier to reason about now
Handle operator ordering - SameValue
function addCelciusFahrenheit(a, b) { ... }
new Celsuis(18) + 4 // This is probably desirable
Handle operator ordering - Typed
class Celsius {
add(n: Number) { this.c += n }
add(n: Fahrenheit) { this.f + n.toCelsius }
}
new Celsuis(18) + 4 // Combining simple numbers might be helpful!
Sketches
function addPointOrVector(a, b) {
if (a instanceof Point) a = new Vector([a])
if (b instanceof Point) b = new Vector([b])
return new Vector(...a.points, ...b.points])
}
class Vector() {
constructor(points) { ... }
get [Symbol.operator('+')]() { return addPointOrVector }
}
class Point() {
constructor(x, y) { this.x = x, this.y = y }
get [Symbol.operator('+')]() { return addPointOrVector }
}
Point(1, 0) + Point(1, 1) == Vector([[1,1], [1,0]])
Symbol.operator()
- Like Symbol.for
- Throws for unknown operators
Sketches
function addPointOrVector(a, b) {
if (a instanceof Point) a = new Vector([a])
if (b instanceof Point) b = new Vector([b])
return new Vector(...a.points, ...b.points])
}
class Vector() {
constructor(points) { ... }
+Point(point) { return addPointOrVector(this, point) }
+Vector(point) { return addPointOrVector(this, point) }
}
class Point() {
constructor(x, y) { this.x = x, this.y = y }
+Point(point) { return addPointOrVector(this, point) }
+Number(number) { return new Point(this.x + number, this.y + number) }
}
Point(1, 0) + Point(1, 1) == Vector([[1,1], [1,0]])
Point(1, 0) + 1 == Point(2, 1)
[Operator][Constructor]()
- Like a typed method
- Specific to operator overloading
- Allows for polymorphic single dispatch
Annex: Proposed semantics for existing builtins
- Have a `GetOperator` spec, similar to `ToString`
- Explicitly does operations to mitigate perf on primivites
- Means for e.g. Number.prototype[Symbol.operator()] does not work
Annex: Now I can no longer reason about my code
a + 'foo' // what does this do?
a + b // what about this?
b + a == a + b // + is not commutative
a = { [Symbol.toPrimitive()]() { fetch('allthebadthings') } }
a + b // Possible to sneak side effects in today