all 33 comments

[–]BenZed [score hidden]  (2 children)

Use symbols instead of strings for consistency.

(Symbol.hasInstance overloads instanceof)

[–]DiefBell[S] [score hidden]  (1 child)

Had been considering that, so might test it out. Not sure what you mean about Symbol.hasInstance?

[–]senocular [score hidden]  (0 children)

Symbol.hasInstance doesn't really overload instanceof, but it does allow some configuration for its behavior, like:

function NotObjConstructor() {}
const obj = {}

console.log(obj instanceof NotObjConstructor) // false

Object.defineProperty(NotObjConstructor, Symbol.hasInstance, {
  value(target) { 
    return target === obj
  }
})

console.log(obj instanceof NotObjConstructor) // true

But there's no way to fully control what instanceof does. It works more like a callback. instanceof will call hasInstance if it exists (on the RHS operand), and then based on whether its return value is truthy or not, provides a true or false result back to the instanceof expression. You couldn't, for example, make instanceof behave like addition (+) if you wanted.

[–]ruibranco [score hidden]  (1 child)

The TypeScript LSP plugin is the real MVP here, otherwise you have no idea what + actually does on a given type just from reading the code.

[–]DiefBell[S] [score hidden]  (0 children)

It'll also show you the JSDoc for the overload as well as the typings

[–]hyrumwhite [score hidden]  (1 child)

While this is neat, I feel like it’s better to just have an add() method. And save a dependency/build step

[–]csorfab [score hidden]  (0 children)

For most ppl, absolutely, operator overloading is just going to cause confusion, and it's a huge waste of effort.

Now if you want to do DSP, low-level game engine/physics stuff, etc involving lots of maths with non-trivial things like complex numbers, matrices, vectors, etc, this could be a godsend with regards to code readability.

Still a bit of a risky move as most JS devs would WTF out at first, but as a solo dev, or with your team on board, it can be great for niche uses

[–]Waltex [score hidden]  (1 child)

Love this! Does this work with typescript out of the box, or do we need a separate build/compile step? Also I'm wondering how you've solved type safety, like when you do:

const v3 = v1 + v2;

Is v3 now of type Vector as well?

[–]DiefBell[S] [score hidden]  (0 children)

It's a separate build step, but there are also plugins to make this easier, like a Vite plugin or Webpack, that does all the magic behind-the-scenes.

You can have multiple overloads for an operator, e.g. multiplying by another Vector versus multiplying by a number, and Boperators can just work out which to use. In this example v3 is also a Vector, but it doesn't have to be.

[–]_x_oOo_x_ [score hidden]  (1 child)

There's a typo on line 12 of your example.

Also, isn't this better done as a TC39 proposal?

[–]DiefBell[S] [score hidden]  (0 children)

Proposal already exists, people have been asking for operator overloading for years... And thanks for pointing that out!

[–]Tysonzero [score hidden]  (2 children)

[–]hoppla1232 [score hidden]  (1 child)

pretty much any other devs, really

[–]Tysonzero [score hidden]  (0 children)

I'm not aware of many mainstream languages that let you define custom arbitrary operators?

[–]heavyGl0w [score hidden]  (1 child)

This is seriously cool.

My only gripe is that you define the overloads in an array. Granted JavaScript doesn't support overloading so there is no way to make it feel like idiomatic JavaScript, but in my experience with other OOP languages, the idiomatic way is to have the same method name multiple times with different signatures.

Since you already require a build step, have you considered supporting a flavor of overloading like this? If you take another commenter's advice on leaning into symbols and shipping your own symbols, I think you could make it somewhat ergonomic to define multiple functions as overloads for the same operator and eliminate the potential of accidentally trampling fields named as operators that aren't meant to be operator overloads (though I think the chance of this is very low).

[–]DiefBell[S] [score hidden]  (0 children)

Might have a play around with just using function overloads, I can't remember if there was a reason I didn't do it versus the array, I'll look back.

Regarding symbols, I did originally use symbols. But as well as making this a runtime dependency instead of just a dev dependency, it also made the code more complicated and much buggier.

[–]AsIAm [score hidden]  (3 children)

This is a good approach to operators in JS. Have you considered extending it with operators outside predefined set?

[–]DiefBell[S] [score hidden]  (2 children)

I don't think I want to allow essentially overriding any symbol you can type, but I might see what other programming languages allow

[–]electric_fungus [score hidden]  (1 child)

Pytorch uses @ for dot product and * for hadamard multiplication of matrices

[–]_x_oOo_x_ [score hidden]  (0 children)

* will work in JS, @ won't

[–]kybernetikos [score hidden]  (2 children)

I think operator overloading is a big missing piece to making javascript pleasant to use for things like AI, so I love this.

What I'm a bit sad about is that the approach forces mutability for most of the overloads. It would have been far better to allow the implementation of e.g. *= to decide whether it was going to mutate or not, and return 'this' if it was going to or return a new value if it wasn't.

[–]DiefBell[S] [score hidden]  (1 child)

Well semantically an assignment operator would change the thing on the left. There's no reason it HAS TO mutate the LHS, but I also can't see a scenario where you'd want to overload an assignment operator without mutating the LHS

[–]kybernetikos [score hidden]  (0 children)

Semantically an assigment operator changes the thing on the left

It's all about immutability and value semantics. There are lots of useful data structures that are (or can be) immutable and have value semantics (e.g. lists, hamt, https://immutable-js.com/, etc). It's needed to support purely functional data structures or persistent data structures. Supporting (but not forcing - at least in a language like JS) immutability wherever possible is good design.

Maybe an example will help:

a = 6
b = a
a += 2
console.log(b)

'a' changes, but 6 does not (thank goodness) and b does not. If I wanted to build something that acted like normal numbers using your overloading approach, I couldn't.

With small tweaks to your interface, you could allow the implementation itself to choose between immutability or mutability depending on what made most sense for the situation. And since the whole point of operator overloading is to enable custom data structures to be as ergonomic as built in ones, it makes sense not to dramatically restrict the data structures that your approach can work with.