tc39 / proposal-operator-overloading

MIT License
618 stars 20 forks source link

Use a mathematical definition of operators, decoupled from class definitions #44

Open PaulKiddle opened 3 years ago

PaulKiddle commented 3 years ago

In mathematics, an operator is essentially a function that maps elements from one set to a single element in another (or the same) set [mathematicians please excuse my over-simplification]. The operator is not a property of any set, but the definition of the sets is a prerequisite to the definition of the operator.

Taking this kind of direction in javascript, an operator overload would not be defined as a property of a class, but as a function that checks its arguments' types and performs the appropriate calculations based on that.

A key advantage of this is that a user could define an operator over two classes or modules from different sources, that would not be aware of each other by default:

import Complex from 'complex-numbers';
import Vector from 'vectors';

const [operator *] = function (left, right) {
  if(left instanceof Complex && right instanceof Vector) {
    const [complex, vector] = [left, right];
    return new Vector(vector.elements.map(vectorElement => complex.multiplyBy(vectorElement)))
  }
  // Include some way of falling back to default either explicitly or implicitly
  return super(complex, vector);
}

const complex = Complex('i + 5');
const vector = Vector([0, 1, 2]);

const result = complex * vector;

// Result is Vector([0, Complex('i+5'), Complex('2i+10')])
console.log(result);

This kind of implementation probably would require that operators work with scopes and closures in the same way that const or let variables are, in order to stop overloading affecting other areas of the code unintentionally. But I don't think that's a problem (in fact it could be considered improving code clarity anyway).

jvcmarcenes commented 3 years ago

I feel that operator overloading should not be defined in such absolute terms. Say you were using two libraries that overloaded the same operator, there would be a conflict. I believe that a better approach could be too define an "operator" for given types, and the operator function would only be applied if the arguments matched the defined types. Such approach would have a "composition over inheritance" model, where as in your proposed syntax, operators would override their parents, and run imperative code.

This is a proposed syntax:

import Complex from 'complex-numbers'
import Vector from 'vectors'

operator (Complex com * Vector vec) {
  return new Vector(vec.elements.map(element => com.multiplyBy(element)))
}

The operator definition could be preceeded by 'export', too make the operator available to other modules.

import Complex, (Complex * any) from 'complex-numbers'
import Vector from 'vectors'

export operator (Complex com * Vector vec) {
  // we can do 'com * element' since we imported the (Complex * any) operator from complex-numbers
  return new Vector(vec.elements.map(element => com * element))
}

Additional syntax could be use to mark an operator as comutative. operator (Complex com comutative* Vector vec) { ... }

This syntax could also allow for ternary operators.

operator (Number x < Number y < Number z) {
  return (x < y) && (y < z)
}

And for new operators.

operator (any x |> Function f) {
  return f(x)
}

"Hello World!" |> console.log
PaulKiddle commented 3 years ago

Yes, ultimately this is the same idea I was trying to portray of separating operator definition from type definition.

There are other questions still to solve such as how to scope the operator, whether to enable duck-typing instead of instanceof checking, etc. But I think this is a good direction.

jvcmarcenes commented 3 years ago

Yes, ultimately this is the same idea I was trying to portray of separating operator definition from type definition.

There are other questions still to solve such as how to scope the operator, whether to enable duck-typing instead of instanceof checking, etc. But I think this is a good direction.

the original proposal proposes a with operator from 'module' syntax I believe this could be used here too. So instead of importing the operator, you'd declare it within the scope with with