tc39 / proposal-operator-overloading

MIT License
617 stars 20 forks source link

[IDEA] what is we remove commutativity ? #25

Open lifaon74 opened 4 years ago

lifaon74 commented 4 years ago

Some operations like: +, * or == are commutative (a + b = b + a) but some are not, like - or / (a - b != b - a).

INFO: commutative with some limits because js accept really strange stuff to append with operators and different type operands like [] + (() =>{}), and strings concatenation is not commutative.

The commutativity forces the introduction of the left and right property provided to the Operator function.

What if, instead, we consider that no operator are commutable when overloading ? The left always "win" and does the overloading.

Example (the "plus" method here is totally abstract and doesn't represent a real property, but the "+" overloading function):

Decimal(1) + Decimal(2)
// kind of equivalent of
Decimal(1).plus(Decimal(2))

Decimal(1) + 2
// =>
Decimal(1).plus(2)

1 + Decimal(2)
// =>
(1).plus(Decimal(2)) // never Decimal(2).plus(1)

// so:
Decimal(1) + 2 // => Decimal(3)
1 + Decimal(2) // => 3

// => Decimal(1) + 2 is not necessary equal to 1 + Decimal(2) 
// by default the "plus" method of a <number> would reflect the default current behavior. So the valueOf() method of Decimal would be called.

This could simplify a lot the operator overloading, and acknowledge the developers that the left value is always prevalent, so the left operator overloading will be used. This way, we know precisely what would append when we do type1 + type 2 => type1.plus(type2) and never type2.plus(type1) due to an "hidden" right property.

Of course, this restrict a little our usages but ensures (according to me) that we always have the same behavior: left overloads the operator, instead of having a potential "unpredicted" behavior if a right exists.

So yes, Decimal(1) == 1 may be potentially different than 1 == Decimal(1). It may sound strange, but makes sense if we consider operator overloading "converting " it to Decimal(1).equals(1) and (1).equals(Decimal(1)) AND we will be aware of this because we imported the operator overloading using with operators from Decimal;

Let's share your feelings on it.

trusktr commented 4 years ago

What if an overload can specify the commutability somehow?

lifaon74 commented 4 years ago

Yes, this is currently what this proposal does with 'left' and 'right', but on my opinion this add a lot of complexity on the overloading code

littledan commented 4 years ago

This proposal doesn't enforce commutativity, so I'm not sure what you're proposing here.

lifaon74 commented 4 years ago

So let's recap:

Currently this proposal "patches" the commutativity of some operators like +, ==, === etc... with the 'left' and 'right' properties. It's just my own opinion, but I find it adds a lot of complexity and may be a source of errors or potential overload conflicts if not handled with care:

const DecimalOperators = Operators({
  "=="(a, b) { return a._big.eq(b._big); },
}, {
  left: Number, {
    "=="(a, b) { return b._big.eq(a); }
  }
}, {
  right: Number, {
    "=="(a, b) { return a._big.eq(b); }
  }
});

Here the '==' may potentially be resolved by 3 manners.

So my idea is to "remove" the commutativity when overloading => only the left variable (ex: a == b) overload operator must be called (my opinion).

This translates to a pseudo (not actual code): a.equals(b) and nerver to b.equals(a)

So we may simplify the operator overloading like this:

const DecimalOperators = Operators({
  "=="(a, b) { return a.equals(b); }, // where the 'equals' function supports 'b' as a BigDecimal, a Number, a BigInt, etc...
});

If 'a' has an overload '==' operator, but 'b' doesn't then b == a translates to the defaults js '==' operation instead of calling the 'right'.'==' operator of 'a'.

Moreover this will simplify potential conflicts / unwanted behaviors such as: 'a' and 'b' have a '==' overload for both 'left' and 'right' but with a different implementation.

The final idea is to get rid of the 'left' and 'right' properties, which are not necessary if we remove the commutativity when overloading.

littledan commented 4 years ago

The README explains use cases which benefit from overloading for both the left and right operands, and these cases are not necessarily commutative. Are you suggesting that we consider these applications out of scope?

lifaon74 commented 4 years ago

If we remove the 'commutativity' (only for overloading, I repeat) the left and right are no more really useful as we may check ourself the type

littledan commented 4 years ago

I'm still having trouble understanding the idea. Maybe you could write up some of the examples in the README in terms of your concept, in terms of how someone would overload operators on Decimal?

lifaon74 commented 4 years ago

So the concept is:

a == b always translate to:

And never to: if 'a' hasn't == overload, calls 'b' overload

littledan commented 4 years ago

How would this enable us to handle things like 3 * vector?

lifaon74 commented 4 years ago

So this translate to (3).times(vector) (pseudo code) meaning that we first check if 3 has a 'times' overload operator. Because it hasn't, the default js '' is used, resulting in NaN ! So as i said, there is not commutativity for the overloading. YES this restrict usages, but it avoids conflicts (let say we have a mat3 from lib 'a' and vec3 from lib 'b', who should win when doing mat3 vec3) and avoids using left and right

littledan commented 4 years ago

OK, interesting idea. If we consider this out of scope, it would indeed simplify the proposal significantly.

lifaon74 commented 4 years ago

Yes that's the purpose: simplify things for both implementers and developers, by just removing commutativity. It's not a big deal from my point of view if it may avoid errors and bugs creation

littledan commented 4 years ago

I don't think this property is commutativity (which no one has proposed guaranteeing). I think it would make sense to refer to it as, "dispatch on the right operand".

hax commented 3 years ago

If only dispatch on left, it become a bit like the symbol-based solution in some degree...

lifaon74 commented 3 years ago

Not really, they created a "class" approach to hide the overload instead of leacking properties, etc...

hax commented 3 years ago

Yeah I understand the difference, what I mean is, when we simplify the feature, many people would ask why we not use the simplest way? 😂

pedropaulosuzuki commented 3 years ago

Not having 3 * vector(1, 2, 3) kind of defeats the purpose though

lifaon74 commented 3 years ago

Well, in most other languages, operators overloading works only on the left side to avoid "conflics". Its ensures known behaviour instead of potential hidden right overload. The purpose is to avoid errors, by forcing the developers to known that only the left variable may overload the operator. The only difference is that on other languages, they got a compile error in the case of 3 * Vector (or must do tricky stuff)

pedropaulosuzuki commented 3 years ago

Well, in most other languages, operators overloading works only on the left side to avoid "conflics". Its ensures known behaviour instead of potential hidden right overload. The purpose is to avoid errors, by forcing the developers to known that only the left variable may overload the operator. The only difference is that on other languages, they got a compile error in the case of 3 * Vector (or must do tricky stuff)

The thing is that 3 is a Number type, and we kind of don't need to overload numeric types. So they could be an exception, where if the type is a number (either 64 bit float or bigInt), you use the other one, otherwise we follow that rule.

littledan commented 3 years ago

@lifaon74 Which languages are you thinking of? Smalltalk works that way, but most languages that I looked at with operator overloading try to solve the problem of dispatching on both operands, one way or another.

lifaon74 commented 3 years ago

This issue may help: https://github.com/tc39/proposal-operator-overloading/issues/32

In scala, rust or even cpp, only left overload exists (a trick may do the job in cpp but it's very unrecommanded)

jhmaster2000 commented 2 years ago

Well, in most other languages, operators overloading works only on the left side to avoid "conflics". Its ensures known behaviour instead of potential hidden right overload. The purpose is to avoid errors, by forcing the developers to known that only the left variable may overload the operator. The only difference is that on other languages, they got a compile error in the case of 3 * Vector (or must do tricky stuff)

The thing is that 3 is a Number type, and we kind of don't need to overload numeric types. So they could be an exception, where if the type is a number (either 64 bit float or bigInt), you use the other one, otherwise we follow that rule.

I like this idea, it can be taken a step further to the general definition of (a OP b):

Pseudocode JS implementation of the above logic for better overview:

function evaluateBinaryOperator(op, left, right) {
  // IsPrimitive is a built-in native function of JS engines
  if (IsPrimitive(left)) {
    if (IsPrimitive(right)) return BuiltinJSOperators[op](left, right);
    if ([[operator(op)]] in right) return right.[[operator(op)]](left, right);
    else return BuiltinJSOperators[op](left, right);
  } else {
    if ([[operator(op)]] in left) return left.[[operator(op)]](left, right);
    else return BuiltinJSOperators[op](left, right);
  }
}

This allows for usecases such as 3 + Vector to work by simply having the + operator overloaded in Vector, whilst still keeping predictability and disambiguity when evaluating expressions such as Matrix + Vector where both classes overload the + operator, where it will simply invoke Matrix's overload. This also saves us from the overly complex and rather ugly-looking syntax the current proposal has, without sacrificing it's benefits (right operand dispatch) entirely.