Re: [PHP-DEV] Operator overloading for userspace objects

This is only part of a thread. view whole thread
  108421
February 6, 2020 23:36 johannes@schlueters.de (Johannes =?ISO-8859-1?Q?Schl=FCter?=)
On Wed, 2020-01-29 at 00:14 +0100, jan.h.boehmer@gmx.de wrote:
> the last days I have experimented a bit with operator overloading in > userspace classes (redefing the meaning of arithmetic operations like
Some historic context: I am probably the one who did operator overloading in PHP first. Oldest trace is this post: https://markmail.org/message/y7rq5vcd5ucsbcyb This example can be used to show a major problem for PHP doing this. The first problem is that PHP historically had very few type annotations making the code hard to predict, modern PHP has more of that reducing this a bit, but the big problem remains: In PHP we can't overload functions, thus operators have to be member functions and therefore form a closed set. In your example the vector3 can operate on vector3s. $a = new Vector3(1, 2, 3); $b = new Vector3(3, 2, 1); $c = $a * $b; Within Vector3 that is complete. But maths allows multiplication with integers. So, yes, you can extend your __mul() with a check for the rhs as you did with the is_numeric, but why would $c = 2 * $a; call the Vecotr3's operator function? I believe it would call integer's operator. Which obviously doesn't exist. But okay, let's do a hack for integer, to call the second arguments operator if first argument is an integer. Now I come and really like your vector3 library and create my Matrix type. With my Matrix i want to still use your Vector3. include 'your/vector3.php'; class Matrix { public static function __mul($lhs, $rhs) { ... } } $vec = new Vector3(...); $matrix = new Matrix(...); $result = $vec * $matrix; Which one is being called? - Vector's or Matrix's. How will your vector know about my Matrix? The way C++ solves this is by allowing non-member functions as operators. #include "vector3.h" // provides class Vector3 #include "matrix.h" // provides class Matrix, potentially // from a different vendor Matrix operator*(const Vector3 &lhs, const Matrix &rhs) { // I can provide this myself if neither Vctor's nor // Matrix's vendor do return ...; } int main() { Vector3 vec{...}; Matrix matrix{....}; // works auto result = vec * matrix; } To make this really work C++ has another magic, aside from function overloading, which is ADL - Argument depending lookup, which is the black magic of C++: If The function to be called is not only looked for in the current or global namespace, but also the namespace of the first Argument. So this works: namespace JohannesCoolLibrary { class Vector; void func(Vector v); Vector operator*(Vector lhs, Vector rhs); } namespace SomeOtherCoolThing { JohannesCoolLibrary::Vector v1{}; JohannesCoolLibrary::Vector v2{}; v1 * v2; // will find the operator in the namespace // even though it's using the global name func(v1); // will also call function from argument's namnespace } Without these features you can only create closed types, which massively limit interoperability, which massively limits the use cases for operator overloading. With limited set of use cases, this is a rare feature, which is hard to understand, or how many internals reads do you expect immediately know the output of function a($a, $b) { return $a + $b; } var_dump(a([1], [2])); johannes
  108425
February 7, 2020 20:48 ajf@ajf.me (Andrea Faulds)
Hi Johannes,

Thank you for your points! I think you point out some overlooked issues.

Johannes Schlüter wrote:
> Which one is being called? - Vector's or Matrix's. How will your > vector know about my Matrix? > > The way C++ solves this is by allowing non-member functions as > operators. > > #include "vector3.h" // provides class Vector3 > #include "matrix.h" // provides class Matrix, potentially > // from a different vendor > > Matrix operator*(const Vector3 &lhs, const Matrix &rhs) { > // I can provide this myself if neither Vctor's nor > // Matrix's vendor do > return ...; > } > > int main() { > Vector3 vec{...}; > Matrix matrix{....}; > > // works > auto result = vec * matrix; > }
I wonder if it would be a good idea, if we do want operator overloading in PHP, to implement a similar mechanism for this. Perhaps type-specific overloads could be registered via some special function call or declaration, something vaguely like: class Vector { public function __construct() { php\register_overload($this, Matrix::class, '*', function ($a, $b) { /* multiplication implementation here */ }); } } The engine could then do type-matching for you, and would implement commutativity for you if the operator is commutative, so `$someVector * $someMatrix` would call the above function, but so would `$someMatrix * $someVector`. (Note: to support matrix multiplication, I guess commutativity must be overridable. Also, I have forgotten whether multiplying a matrix and a vector is commutative or not :p) I think this approach would be less messy than having to implement full type matching on both sides of a type pair, for a number of reasons: * Instead of Vector having to have an implementation of __mul which checks for Matrix, and Matrix having to have an implementation of __mul which checks for Vector, just one of these types can call register_overload with a single implementation (because the operation is commutative). * Whether two types can be used with a particular operator is clear: either there is such a pair registered, or there is not, and PHP can give appropriate error messages. It is unlikely there will be an issue where one side has __mul but it just throws an exception or somethig. Also, in a case like `$a * $b`, $a can implement support for $b without $b having to support $a, while at the same time $b can implement support for some unrelated other type, without `$b * $a` not working (with the current proposal, imagine $a's __mul handler supporting $b but not vice-versa). * The engine can see conflicts (one class declares an overload involving the other class, and vice-versa) and warn about them, rather than `$a * $b` silently having completely different behaviour to `$b * $a`. This is not to say we necessarily should implement this, but it may be worth thinking about… Thanks, Andrea