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

Questions?

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