Hi internals,
I have opened voting on
https://wiki.php.net/rfc/userspace_operator_overloading, which allows users
to overload operators in their own classes.
Voting closes on 2020-04-06.
Regards,
Jan Böhmer
Hey Jan,
Just posting here why I voted "no": it is not your implementation proposal,
but rather the concept per-se that IMO shouldn't land in the language.
Operator overloading makes call-site code reading extremely hard, and it
makes the language much more complex for very little benefit.
Everything suggested in the RFC can be done by using explicit arrows: ->
(method calls), which lead to expressively named methods and parameters.
I have posted similar thoughts about ->__toString()
and ->toString()
when it comes to cast operations vs explicit calls at
https://github.com/ShittySoft/symfony-live-berlin-2018-doctrine-tutorial/pull/3#issuecomment-460441229
Overall, without type classes and infix functions, operator overloading is,
IMO, just messy.
Greets,
Marco Pivetta
Hi internals,
I have opened voting on
https://wiki.php.net/rfc/userspace_operator_overloading, which allows
users
to overload operators in their own classes.Voting closes on 2020-04-06.
Regards,
Jan Böhmer
Am 24.03.2020 um 11:03 schrieb Marco Pivetta:
Just posting here why I voted "no": it is not your implementation proposal,
but rather the concept per-se that IMO shouldn't land in the language.
I voted "no" for the same reason.
Am 24.03.2020 um 11:06 schrieb Sebastian Bergmann:
I voted "no" for the same reason.
I changed my vote to "yes" because of Nikita's arguments.
I accept that it adds an extra level of understanding to the language – if
you see
function addAmounts($a, $b) { return $a + $b; }
you no longer definitely know the answer will be numeric.
However, I imagine that the sorts of people who will use this are also
the sorts of people who would add type annotations clearly – so you'd much
more likely see
function addAmounts(CurrencyAmount $a, CurrencyAmount $b) { return $a + $b;
}
where it's trivial to understand that there's userspace operator
overloading going on.
Hey Jan,
Just posting here why I voted "no": it is not your implementation proposal,
but rather the concept per-se that IMO shouldn't land in the language.Operator overloading makes call-site code reading extremely hard, and it
makes the language much more complex for very little benefit.Everything suggested in the RFC can be done by using explicit arrows:
->
(method calls), which lead to expressively named methods and parameters.I have posted similar thoughts about
->__toString()
and->toString()
when it comes to cast operations vs explicit calls atOverall, without type classes and infix functions, operator overloading is,
IMO, just messy.Greets,
Marco Pivetta
Hi internals,
I have opened voting on
https://wiki.php.net/rfc/userspace_operator_overloading, which allows
users
to overload operators in their own classes.Voting closes on 2020-04-06.
Regards,
Jan Böhmer
Hi internals,
I have opened voting on
https://wiki.php.net/rfc/userspace_operator_overloading, which allows
users
to overload operators in their own classes.Voting closes on 2020-04-06.
Regards,
Jan Böhmer
To offer a counter-point, I voted yes on this proposal. A couple of
thoughts:
-
This is exposing functionality that already exists for internal classes
to userland classes. As a general rule, I think we should always strive to
have behavioral parity between internal and userland classes. -
Because this just exposes existing functionality, the amount of
technical complexity this introduces is very small, especially compared to
the significance of what this enables. The implementation complexity of
RFCs is increasingly becoming a problem, due to the complex way in which
existing language features interact. This RFC does not fall into that
category. -
As mentioned, this functionality already exists internally and is used
by GMP, where it works (imho) very well. Of course, this is also something
of a poster-child use case for operator overloading, in that the object
literally represents a number. But there are plenty other similar examples,
some of them very relevant to PHP (money represention).
I think there is a somewhat irrational fear that operator overloading is
going to be misused... but having worked in quite a few languages that do
support operator overloading, I think I've only encountered this in two
instances: First, the infamous use of << and >> as stream operators in C++
(which is really horrible, and gives operator overloading a bad name
everywhere). And second, the use of / as a path concatenation operator in
some libraries. And I think that's it. All other uses I've worked with were
things like arbitrary-precision integers, floats, decimals or rationals;
complex numbers, vectors, matrixes or tensors; bit vectors, constant
ranges, known bits representations.
Operator overloading is not a feature that is commonly needed or should be
used much, but it does tend to make code a lot more readable in the cases
where it is useful.
I think people might also find this blog post by Guido interesting:
https://neopythonic.blogspot.com/2019/03/why-operators-are-useful.html
Regards,
Nikita
- This is exposing functionality that already exists for internal classes
- Because this just exposes existing functionality, the amount of
technical complexity this introduces is very small
- As mentioned, this functionality already exists internally and is used
by GMP, where it works (imho) very well.
Thanks Nikita for the insights, that's really helpful.
I'd suggest removing the "PHP_OPERAND_TYPES_NOT_SUPPORTED" constant and
settle on null instead.
The reason is visible in the RFC: public static function __mul($lhs, $rhs): ?Vector3
- nothing else than null can be returned here when looking at the
return type. Returning a const is just calling for "WTF" indirections IMHO.
I'm still voting yes, hoping for this const to be removed after if the RFC
passes, if it does :)
Nicolas
I have opened voting on
https://wiki.php.net/rfc/userspace_operator_overloading, which allows users
to overload operators in their own classes.
I am certainly not opposed to operator overloading, and would like to
see it in the language.
However, I'll be voting no because of a couple of implementation points:
-
I feel like the choice of
NULL
as the return type for not implemented
is far from ideal, especially for 8.0 which provides for union type hints. -
The error handling behaviour is too timid. If operator overloading is
to become a first-class feature, error handling should reflect as such
and any attempt to use objects without the appropriate handlers being
installed really should result in an error being thrown just as it would
be if an undefined method was called.
Mark Randall
Hi internals,
I have opened voting on
https://wiki.php.net/rfc/userspace_operator_overloading, which allows
users
to overload operators in their own classes.Voting closes on 2020-04-06.
Regards,
Jan Böhmer
Thank you.
I voted yes on this for similar reasons than Nikita. I think it is
confusing to users why internal objects can overload these and userland
can't.
The simplicity of exposing this is another plus.
The potential for misuse is a non issue for me, because essentially this
applies to most features, especially with existing magic methods we have a
precedent that we can build upon instead of "repressing use".
I have opened voting on
https://wiki.php.net/rfc/userspace_operator_overloading, which allows users
to overload operators in their own classes.
It seems to me that the RFC is not sufficiently specific enough
regarding the concatenation of instances of classes which implement
__toString(). The RFC itself doesn't mention that explicitly, and the
backward incompatible changes section states:
| As long as the user does not implement, the operator magic functions,
| operators on objects will behave in the previous way.
However, at least the current implementation raises two notices ("You
have to implement the __concat function") when concatenating Stringable
objects.
A minor issue: in my opinion, introducing
PHP_OPERAND_TYPES_NOT_SUPPORTED doesn't make sense. The RFC says:
| Handlers can specify return typehints, but note that the return type
| has to be nullable (as PHP_OPERAND_TYPES_NOT_SUPPORTED has the value
| null).
So if we ever wanted to change the value of
PHP_OPERAND_TYPES_NOT_SUPPORTED, we'd introduce a BC break. Therefore
the constant's value likely will never change, so using NULL
directly to
signal unsupported operand types would be fine, wouldn't it?
--
Christoph M. Becker
It seems to me that the RFC is not sufficiently specific enough
regarding the concatenation of instances of classes which implement
__toString().
Exactly what I was thinking too. Would be nice with some examples on this.
So if we ever wanted to change the value of
PHP_OPERAND_TYPES_NOT_SUPPORTED, we'd introduce a BC break. Therefore
the constant's value likely will never change, so usingNULL
directly to
signal unsupported operand types would be fine, wouldn't it?
Agree on this too.
It seems to me that the RFC is not sufficiently specific enough
regarding the concatenation of instances of classes which implement
__toString().Exactly what I was thinking too. Would be nice with some examples on this.
I am not sure if I can (or should) still add this to the RFC, here some clarification on this:
The overloaded concat operator has higher priority than the __toString() method.
So if Class A overloades the concat operator, then calling $a . $b means ClassA::__concat($a, $b); (Note that both operands are passed in their original form)
If you want to concat the string representations, you will have to explicitly convert the objects to strings:
$ret = (string) $a . (string) $b;
If the concat operator is not overloaded, the behavior is like now, and the objects are converted implicitly to strings (so $a . $b actually means (string) $a . (string) $b).
Furthermore an notice is triggered, hinting the user that he could overload the concat operator. (Maybe here a different message than for the other operators would be useful).
The overloaded concat operator has higher priority than the __toString() method.
So if Class A overloades the concat operator, then calling $a . $b means ClassA::__concat($a, $b); (Note that both operands are passed in their original form)
If you want to concat the string representations, you will have to explicitly convert the objects to strings:
$ret = (string) $a . (string) $b;
This first part seems legit to me.
If the concat operator is not overloaded, the behavior is like now, and the objects are converted implicitly to strings (so $a . $b actually means (string) $a . (string) $b).
Furthermore an notice is triggered, hinting the user that he could overload the concat operator. (Maybe here a different message than for the other operators would be useful).
I fear that "hint" notice could break Symfony apps... Couldn't you
just not trigger it in this case?
--
Guilliam Xavier
The overloaded concat operator has higher priority than the __toString() method.
So if Class A overloades the concat operator, then calling $a . $b means ClassA::__concat($a, $b); (Note that both operands are passed in their original form)
If you want to concat the string representations, you will have to explicitly convert the objects to strings:
$ret = (string) $a . (string) $b;This first part seems legit to me.
If the concat operator is not overloaded, the behavior is like now, and the objects are converted implicitly to strings (so $a . $b actually means (string) $a . (string) $b).
Furthermore an notice is triggered, hinting the user that he could overload the concat operator. (Maybe here a different message than for the other operators would be useful).I fear that "hint" notice could break Symfony apps... Couldn't you
just not trigger it in this case?
Either that (it's what I would prefer), or clearly document that BC
break (even if it's just about a notice).
Thanks,
Christoph
If the concat operator is not overloaded, the behavior is like now, and the objects are converted implicitly to strings (so $a . $b actually means (string) $a . (string) $b).
Furthermore an notice is triggered, hinting the user that he could overload the concat operator. (Maybe here a different message than for the other operators would be useful).I fear that "hint" notice could break Symfony apps... Couldn't you just not trigger it in this case?
Yes this would be possible and I think that it might be reasonable to omit the notice in this case (maybe only if the objects really implement a __toString).
What do others think about this?
Regards,
Jan
Hi, internals!
I want to mention, that all existing internal API of Zend could be
accessible via FFI as of PHP7.4. This gives opportunity to implement
userspace operator overloading as a simple PHP package.
I know, that FFI requires some polishing, but it could become a tool to
create interesting extensions via Z-Engine or similar libraries. Just for
example, I have a repository
https://github.com/lisachenko/native-types which provides an example of
userspace operator-overloading for matrices (see __doOperation method
implementation at
https://github.com/lisachenko/native-types/blob/master/src/Matrix.php#L232-L276
)
Please share you thoughts about this.
Best regards,
Alexander
пн, 23 мар. 2020 г. в 20:58, jan.h.boehmer@gmx.de:
Hi internals,
I have opened voting on
https://wiki.php.net/rfc/userspace_operator_overloading, which allows
users
to overload operators in their own classes.Voting closes on 2020-04-06.
Regards,
Jan Böhmer
Hi internals,
I have opened voting on
https://wiki.php.net/rfc/userspace_operator_overloading, which allows users
to overload operators in their own classes.
I've always been a strong proponent of operator overloading, but as PHP
already has it internally, it makes sense to also make that information
available for userland.
cheers,
Derick
I have opened voting on
https://wiki.php.net/rfc/userspace_operator_overloading, which allows users
to overload operators in their own classes.I've always been a strong proponent of operator overloading, but as PHP
^^^^^^^^^
I meant "opponent" there of course.
already has it internally, it makes sense to also make that information
available for userland.
In any case, I am a +1!
cheers,
Derick
--
PHP 7.4 Release Manager
Host of PHP Internals News: https://phpinternals.news
Like Xdebug? Consider supporting me: https://xdebug.org/support
https://derickrethans.nl | https://xdebug.org | https://dram.io
twitter: @derickr and @xdebug
pon., 23 mar 2020 o 18:58 jan.h.boehmer@gmx.de napisał(a):
Hi internals,
I have opened voting on
https://wiki.php.net/rfc/userspace_operator_overloading, which allows
users
to overload operators in their own classes.
I got two comments, a little late but always better than even later.
The first thing is for operator methods when the operation is not supported
I would see simply return null;
as the right solution
instead of constant, which name no one will remember.
The second thing is notices which are confusing when suggesting to go
and implement overloaded operator method on extension derived classes
like stdClass like below:
Notice: You have to implement the __add function in class stdClass to use
this operator with an object in...
The text of the notice is not documented in the RFC but it is implemented
that way in the patch.
A PHP developer is not likely gonna download the php-src source code,
build environment and go with the ext/standard implementation and start
adding it in C.
The notice in this cases should either be different or not emitted at all.
If the latter then that has to be documented I guess.
Cheers,
Michał Brzuchalski
Hi,
Michał Brzuchalski wrote:
The first thing is for operator methods when the operation is not supported
I would see simplyreturn null;
as the right solution
instead of constant, which name no one will remember.
If we are to allow arbitrary use of operator overloading (rather than
restricting it to specific kinds of use), which this RFC does, then IMHO
we shouldn't special-case “null” — why can't it be a valid result of an
overloaded operator? It is an arbitrary constraint on what can be done
with operator overloading and makes the feature less generic.
I would prefer if we used a different mechanism, for example a special
subclass of Exception which will be caught internally by the operator
overloading code, so that signalling an operation is unsupported can be
“out of band” if that makes sense.
The second thing is notices which are confusing when suggesting to go
and implement overloaded operator method on extension derived classes
like stdClass like below:Notice: You have to implement the __add function in class stdClass to use
this operator with an object in...The text of the notice is not documented in the RFC but it is implemented
that way in the patch.
Please document all notices your patch produces in the RFC, I think they
are an important detail of the proposal.
A PHP developer is not likely gonna download the php-src source code,
build environment and go with the ext/standard implementation and start
adding it in C.
The notice in this cases should either be different or not emitted at all.
If the latter then that has to be documented I guess.
That sounds reasonable, and it should be simple to add a check to your
patch to make sure it won't show this message for internal/extension
classes, right? By the way, if the operation has two operands, one of
which is an internal/extension class and the other a user class, you
could perhaps still show the mssage, but only mention the user class name.
Regars,
Andrea
Hi internals,
I have opened voting on
https://wiki.php.net/rfc/userspace_operator_overloading, which allows
users to overload operators in their own classes.
I consider operator overlaoding in general a good feature (while PHP's
overload for + on arrays is weird)
However I don't like this design.
Consider this case: Some library provides a type A without overloads. I
then create a type B with overload:
class B {
static public __add($lhs, $rhs) {
if ($lhs instanceof A) ...
elseif($rhs instanceof A) ...
else error
}
}
as I want my type's addition to commutative I handle this with some if
(probably dispatching. Now I can invoke it:
new A() + new B(); // calls B::__add()
new B() + new A(); // calls B::__add() as well
Now the maintainer of A implements the feature request "A should allow
addition with integers", I update my library and suddenly weird things
happen.
new A() + new B(); // calls A::__add() and explodes, as A doesn't know B
new B() + new A(); // calls B::__add() works as before
Now we could establish the best practice that one type's operator calls
the other type's operator in case it can't handle a type. However that
is no good option as it will lead to infinite recursion if neither type
can handle both operands.
The issue is that by forcing type declarations as part of the class, as
members, this forms a closed set, but for many cases a strict closed
set isn't what one wants. For mathematical types having operators
working with integers (and other numeric values) is essential.
Communativity often also is required. So a true closed set doesn't
work, for an open set can't be created easily in today's PHP. Thus the
RFC tries to tie this together, but this will fall apart and then cause
legacy and BC reasons preventing future improvement.
I believe the pre-requisit is having some form of function overloading,
where operator functions for specific argument types can be defined. In
https://news-web.php.net/php.internals/108425 Andrea created an idea,
which is probably "ugly" but has less usage restrictions. I think
spending time on function overloading (I believe, without proving it,
this can be done with very little cost for non-overlaoded cases - by
adding a flag "overloaded" along visibility flags and check that along
with the visibility check, only in case of an overload to the
"expensive" check, which still is cheaper done in the engine than
if/else chains in userspace) and then take up operator overloading
again, rather than this smart but limited approach. (For whoever does
that: spend time in C++ and its function resolution rules incl. ADL,
not to copy, but to learn)
johannes
Hi internals,
I have opened voting on
https://wiki.php.net/rfc/userspace_operator_overloading, which allows
users to overload operators in their own classes.I consider operator overlaoding in general a good feature (while PHP's
overload for + on arrays is weird)However I don't like this design.
Consider this case: Some library provides a type A without overloads. I
then create a type B with overload:class B { static public __add($lhs, $rhs) { if ($lhs instanceof A) ... elseif($rhs instanceof A) ... else error } }
as I want my type's addition to commutative I handle this with some if
(probably dispatching. Now I can invoke it:new A() + new B(); // calls B::__add()
new B() + new A(); // calls B::__add() as wellNow the maintainer of A implements the feature request "A should allow
addition with integers", I update my library and suddenly weird things
happen.new A() + new B(); // calls A::__add() and explodes, as A doesn't know
B
new B() + new A(); // calls B::__add() works as beforeNow we could establish the best practice that one type's operator calls
the other type's operator in case it can't handle a type. However that
is no good option as it will lead to infinite recursion if neither type
can handle both operands.The issue is that by forcing type declarations as part of the class, as
members, this forms a closed set, but for many cases a strict closed
set isn't what one wants. For mathematical types having operators
working with integers (and other numeric values) is essential.
Communativity often also is required. So a true closed set doesn't
work, for an open set can't be created easily in today's PHP. Thus the
RFC tries to tie this together, but this will fall apart and then cause
legacy and BC reasons preventing future improvement.I believe the pre-requisit is having some form of function overloading,
where operator functions for specific argument types can be defined. In
https://news-web.php.net/php.internals/108425 Andrea created an idea,
which is probably "ugly" but has less usage restrictions. I think
spending time on function overloading (I believe, without proving it,
this can be done with very little cost for non-overlaoded cases - by
adding a flag "overloaded" along visibility flags and check that along
with the visibility check, only in case of an overload to the
"expensive" check, which still is cheaper done in the engine than
if/else chains in userspace) and then take up operator overloading
again, rather than this smart but limited approach. (For whoever does
that: spend time in C++ and its function resolution rules incl. ADL,
not to copy, but to learn)johannes
--
This issues become even more apparent when sequencing operations like $a + $b + $c - $d
. Trying left, then trying right, will make it very difficult
to determine the outcome of such a statement.
The arguments against type hinting for operator methods, assume the "try
left/right" method. Instead, type hinting should be applied to determine
which method should be used. If both or neither methods are applicable, an
error must be thrown.
With class inheritance, you also don't want to rely on the order of the
operands. If you're the creator of a library, that's out of your hands. If
both the parent and child class overwrite the operation, and both accept
the operands, then the child class should always be used.
It's a -1 for me. I would vote in favor of an RFC for operator overloading
without the "try left, try right" behavior.
Arnold
This issues become even more apparent when sequencing operations like
$a + $b + $c - $d
. Trying left, then trying right, will make it very difficult
to determine the outcome of such a statement.The arguments against type hinting for operator methods, assume the "try
left/right" method. Instead, type hinting should be applied to determine
which method should be used. If both or neither methods are applicable, an
error must be thrown.
This "try left/right" approach is how operator overloading works for
internal classes[1], and apparently, it works quite well, as long as it
is not overused.
With class inheritance, you also don't want to rely on the order of the
operands. If you're the creator of a library, that's out of your hands. If
both the parent and child class overwrite the operation, and both accept
the operands, then the child class should always be used.
If a subclass overrides the operation, it would have to conform to
parameter type contravariance, so it would always be used, anyway.
[1]
https://github.com/php/php-src/blob/php-7.4.4/Zend/zend_operators.h#L934-L937
--
Christoph M. Becker
Hi,
Christoph M. Becker wrote:
This issues become even more apparent when sequencing operations like
$a + $b + $c - $d
. Trying left, then trying right, will make it very difficult
to determine the outcome of such a statement.The arguments against type hinting for operator methods, assume the "try
left/right" method. Instead, type hinting should be applied to determine
which method should be used. If both or neither methods are applicable, an
error must be thrown.This "try left/right" approach is how operator overloading works for
internal classes[1], and apparently, it works quite well, as long as it
is not overused.
I think “as long as it is not overused” are the key words there. We have
a very limited number of internal classes with operator overloading
right now, and the authors know about what other such classes exist and
therefore can design with them in mind. But once any userland PHP class
can use operator overloading, you could start seeing uses of operators
where the left- and right-hand side are classes from unrelated libraries
whose authors were not aware of eachother, and that could create strange
problems when one or the other library is updated with operator overload
handling.
Consider Arnold's example:
$a + $b + $c - $d
We are not constraining operator overloading to just be for number-like
objects, it can in principle be used for absolutely anything. $a here
might have an overload that works for any right-hand-side object, but
then once $b adds its own similar overload in a new version, the code no
longer does the same thing when it's flipped around ($b + $a), violating
the normal expectation that +
is commutative, and that's before
considering what $c and $d might do!
Thanks,
Andrea
Christoph M. Becker wrote:
This "try left/right" approach is how operator overloading works for
internal classes[1], and apparently, it works quite well, as long as it
is not overused.I think “as long as it is not overused” are the key words there. We have
a very limited number of internal classes with operator overloading
right now, and the authors know about what other such classes exist and
therefore can design with them in mind. But once any userland PHP class
can use operator overloading, you could start seeing uses of operators
where the left- and right-hand side are classes from unrelated libraries
whose authors were not aware of eachother, and that could create strange
problems when one or the other library is updated with operator overload
handling.Consider Arnold's example:
$a + $b + $c - $d
We are not constraining operator overloading to just be for number-like
objects, it can in principle be used for absolutely anything.
And floats can be used to store monetary values, and to do calculations,
and apparently, that is actually done by some. Was it a bad idea to
introduce floats to the language?
$a here
might have an overload that works for any right-hand-side object, but
then once $b adds its own similar overload in a new version, the code no
longer does the same thing when it's flipped around ($b + $a), violating
the normal expectation that+
is commutative, and that's before
considering what $c and $d might do!
If, for example, the + operator is overloaded to add something to a
collection, the normal expectation that + is commutative is already
violated. Operator overloading should definitely not be used for
"anything", but only for those rare cases which resemble math operations
(frankly, I would not have not supported __concat() at all). Also,
overloaded operators should be programmed defensively, i.e. they should
not accept arbitrary arguments (how could that even work?), but only
those they can handle. If implementations adhere to these "rules", I
don't see real issues.
--
Christoph M. Becker
Hi,
Christoph M. Becker wrote:
(frankly, I would not have not supported __concat() at all). Also,
overloaded operators should be programmed defensively, i.e. they should
not accept arbitrary arguments (how could that even work?), but only
those they can handle. If implementations adhere to these "rules", I
don't see real issues.
Consider a type implementing some kind of list. Perhaps someone would
want to overload the + operator to mean adding an item to the list. If
the list accepts any type of value as a valid item, then you have an
example of an unconditional overload.
Regards,
Andrea
Christoph M. Becker wrote:
(frankly, I would not have not supported __concat() at all). Also,
overloaded operators should be programmed defensively, i.e. they should
not accept arbitrary arguments (how could that even work?), but only
those they can handle. If implementations adhere to these "rules", I
don't see real issues.Consider a type implementing some kind of list. Perhaps someone would
want to overload the + operator to mean adding an item to the list. If
the list accepts any type of value as a valid item, then you have an
example of an unconditional overload.
I had already considered this, and wrote immediately above the quote:
If, for example, the + operator is overloaded to add something to a
collection, the normal expectation that + is commutative is already
violated. Operator overloading should definitely not be used for
"anything", but only for those rare cases which resemble math
operations
Thanks,
Christoph
This issues become even more apparent when sequencing operations like
$a + $b + $c - $d
. Trying left, then trying right, will make it very
difficult
to determine the outcome of such a statement.The arguments against type hinting for operator methods, assume the
"try
left/right" method. Instead, type hinting should be applied to
determine
which method should be used. If both or neither methods are
applicable, an
error must be thrown.This "try left/right" approach is how operator overloading works for
internal classes[1], and apparently, it works quite well, as long as it
is not overused.
The fact that it works in one or two cases as an implementation detail where the full implementation is controlled by a single group (internals) is no indication for it to work at large.
With class inheritance, you also don't want to rely on the order of
the
Inheritance is always complex. Yes operator handling (especially if type dispatching happens manually) will add a few things to think about. Certainly an interesting field to explore, before one votes. I myself consider larger inheritance structures a thing of the 90ies and for special cases like GUI widgets ... but yeah, language design has to consider it. Changing languages features like discussed here in future is hard.
johannes
This "try left/right" approach is how operator overloading works for
internal classes[1], and apparently, it works quite well, as long as it
is not overused.The fact that it works in one or two cases as an implementation detail where the full implementation is controlled by a single group (internals) is no indication for it to work at large.
Fair enough. But maybe Python, where userland operator overloading
works similar to the proposal at hand, is? :)
Furthermore, it is already possible to do operator overloading in
userland by using the FFI extension[1], and given that operator
overloading appears to be desired by many developers, I'd rather add it
to the language, then having to deal with libraries which do it via FFI.
[1] https://github.com/lisachenko/z-engine#object-extensions-api
--
Christoph M. Becker
On March 28, 2020 1:25:11 PM GMT+01:00, "Christoph M. Becker" <
cmbecker69@gmx.de> wrote:This "try left/right" approach is how operator overloading works
for
internal classes[1], and apparently, it works quite well, as long
as it
is not overused.The fact that it works in one or two cases as an implementation
detail where the full implementation is controlled by a single
group (internals) is no indication for it to work at large.Fair enough. But maybe Python, where userland operator overloading
works similar to the proposal at hand, is? :)
It doesn't:
Python 3.6.9 (default, Nov 7 2019, 10:44:02)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
class A:
... def add(self, other):
... print("add")
...
a = A()
a + 1
add
1 + a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'A'
johannes
On Sat, Mar 28, 2020 at 5:28 PM Johannes Schlüter johannes@schlueters.de
wrote:
On March 28, 2020 1:25:11 PM GMT+01:00, "Christoph M. Becker" <
cmbecker69@gmx.de> wrote:This "try left/right" approach is how operator overloading works
for
internal classes[1], and apparently, it works quite well, as long
as it
is not overused.The fact that it works in one or two cases as an implementation
detail where the full implementation is controlled by a single
group (internals) is no indication for it to work at large.Fair enough. But maybe Python, where userland operator overloading
works similar to the proposal at hand, is? :)It doesn't:
Python 3.6.9 (default, Nov 7 2019, 10:44:02)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.class A:
... def add(self, other):
... print("add")
...
a = A()
a + 1
add
1 + a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'A'
It does.
Because Python uses instance methods for operator overloading, it has to
use pairs of methods like add and radd to handle commuted variants.
If you want to implement a commutative add, you need to implement both
add and radd. If add is not implemented or returns
NotImplemented, then radd will be called.
So, as Christoph has said, this is indeed the same system as what is being
proposed here, with the difference that we do not need two separate methods
per operator, but can use a static method instead.
Regards,
Nikita
On Sat, Mar 28, 2020 at 5:28 PM Johannes Schlüter
johannes@schlueters.de
wrote:On March 28, 2020 1:25:11 PM GMT+01:00, "Christoph M. Becker" <
cmbecker69@gmx.de> wrote:This "try left/right" approach is how operator overloading
works
for
internal classes[1], and apparently, it works quite well, as
long
as it
is not overused.The fact that it works in one or two cases as an implementation
detail where the full implementation is controlled by a single
group (internals) is no indication for it to work at large.Fair enough. But maybe Python, where userland operator overloading
works similar to the proposal at hand, is? :)It doesn't:
Python 3.6.9 (default, Nov 7 2019, 10:44:02)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more
information.class A:
... def add(self, other):
... print("add")
...
a = A()
a + 1
add
1 + a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'A'It does.
Because Python uses instance methods for operator overloading, it has
to
use pairs of methods like add and radd to handle commuted
variants.
If you want to implement a commutative add, you need to implement both
add and radd. If add is not implemented or returns
NotImplemented, then radd will be called.So, as Christoph has said, this is indeed the same system as what is
being
proposed here, with the difference that we do not need two separate
methods
per operator, but can use a static method instead.
If we use two methods as well it is a saner design. I won't like it, but lot better than the current one.
johannes
Hi everyone,
Johannes Schlüter wrote:
If we use two methods as well it is a saner design. I won't like it, but lot better than the current one.
Just want to +1 this. Two methods, neither of which are static, seems
like a cleaner approach to me. I maybe like this better than my
suggested boolean argument (it certainly looks nicer).
Consider what happens in the current proposal if a class, intentionally
or otherwise, only supports being on one side of an operator… it will
produce the same error message as if it didn't support the overload at
all :(
Thanks,
Andrea
Hi internals,
I have closed the voting. With 38 in favor and 28 against the RFC is DECLINED (didn’t reach the needed 2/3 majority for a new feature).
Thanks to everyone who has participated.
I have closed the voting. With 38 in favor and 28 against the RFC is
DECLINED (didn’t reach the needed 2/3 majority for a new feature).Please don't be discouraged. I did vote against, but I was on the fence
(My vote was 'Yes' for a period). Review the feedback you were offered,
revise the proposal, perhaps even shrink the scope if that seems
appropriate. At 38:28 I don't think we're far off of a version of this
feature which meets with success.
-Sara
Hi internals,
I have closed the voting. With 38 in favor and 28 against the RFC is DECLINED (didn’t reach the needed 2/3 majority for a new feature).
Thanks to everyone who has participated.
Hi Jan,
Thanks for running the RFC. Although it didn't quite pass it seems a
lot closer now.
Apologies for not taking part in the discussion earlier, but I'm slow
at thinking. To follow up on a couple of things.
If it can not handle the given type, it has to return the
constant PHP_OPERAND_TYPES_NOT_SUPPORTED (currently just null).
This does not appear to be a good choice. It means that to evaluate
whether the code is safe to call the code needs to be evaluated. That
makes doing static code analysis very difficult.
Also, it means that operators could not return null as a result of the
operation, which seems like an odd choice that would be a regretful
limitation.
Rowan Tommins wrote from https://externals.io/message/108788#108993 :
$a = new A;
$b = new B;
var_dump($b + $a); # calls B::__add($b, $a); OK
var_dump($a + $b); # calls A::__add($a, $b), which is a TypeError
And jan.h.boehmer@gmx.de wrote:
If an operator handler has typehints, an error will be thrown,
What do others think about this restriction?
I think that is the wrong solution to the wrong problem.
It's a wrong solution because parameter types* are a useful tool both
for program correctness at runtime, they make it easier to run static
code analysis tools on the code and most importantly they save a lot
of developer time, mostly through autocomplete.
It's the wrong problem because that code does have a TypeError. It is
calling '__add' and passing a parameter to the method that the code
can't handle.
It appears to be the same error case as:
class A {
public function add(A $lhs, A $rhs) {...}
}
class B {
public function add(A|B $lhs, A|B $rhs) {...}
}
$a = new A;
$b = new B;
$b->add($a); // Ok
$a->add($b); // TypeError
For me, the solution to the error in this code is not "remove the
parameter type on A::add". I'm pretty sure that the solution to this
type of error would be "fix your code".
When someone looks at the idea of userspace operator overloading
again, I think the following path might reach a more acceptable
solution.
-
Encourage people to use parameter types to make their code easier to
reason about. -
Encourage people to use static analysis to check their code for
errors, including errors where they have invalid operands to
operations. -
Take the approach in the RFC that passing a wrong parameter type to
an operator magic method represents a programming error that cannot be
handled in the normal program flow, and that throwing an exception is
the right thing to do here**. But as most people should be using
parameter types, and static analyzers to check for this type of error,
this should not occur.
Other than a learned reticence against throwing exceptions, what
downsides would that have?
cheers
Dan
Ack
- PHP has parameter types that are checked at runtime, not 'hints'
that can be worked around as found in other programming languages. If
people would stop using the phrase 'type hint', it would make the
conversation more accurate.
** This is not using exceptions for flow control (though it could be
abused for it). Most people should be avoiding these problems through
writing code correctly, and using static analysis to double-check
their code is free from errors.
It appears to be the same error case as:
And that code had a mistake. Should have been:
class A {
public function add(A $rhs) {...}
}
class B {
public function add(A|B $rhs) {...}
}
$a = new A;
$b = new B;
$b->add($a); // Ok
$a->add($b); // TypeError
cheers
Dan
Ack
It appears to be the same error case as:
And that code had a mistake. Should have been:
class A { public function add(A $rhs) {...} } class B { public function add(A|B $rhs) {...} } $a = new A; $b = new B; $b->add($a); // Ok $a->add($b); // TypeError
If we'd go with (dynamic) methods, wouldn't we have to add "r"-methods
(like Python does) as well? And if we did so, wouldn't we encourage
misusing operator overloading for all kinds of stuff (for which it
shouldn't be used, in my opinion)?
--
Christoph M. Becker
On Thu, 9 Apr 2020 at 13:18 (and subsequent correction), Dan Ackroyd <
Danack@basereality.com> wrote:
$a = new A;
$b = new B;
var_dump($b + $a); # calls B::__add($b, $a); OK
var_dump($a + $b); # calls A::__add($a, $b), which is a TypeError... that code does have a TypeError. It is
calling '__add' and passing a parameter to the method that the code
can't handle.It appears to be the same error case as:
class A { public function add(A $rhs) {...} } class B { public function add(A|B $rhs) {...} } $a = new A; $b = new B; $b->add($a); // Ok $a->add($b); // TypeError
As with so much else on this topic, it depends how you think about operator
overloading - and I think that's why it's so hard to agree on an
implementation.
It seems that you're picturing the overloaded + as like a normal method but
with special syntax, so that $a + $b means the same as $a->add($b) and only
that. In that interpretation, it's perfectly reasonable to have the
operation succeed or fail based only on the left-hand operand, because
that's how we're used to method dispatch working.
But if you look at how "normal" operators work, it's far less obvious that
the order of operands should play any role in that decision. For instance,
when mixing float and int, the result is a float if either of the
operands is a float:
var_dump(1 + 1); # int(2)
var_dump(1 + 1.0); # float(2)
var_dump(1.0 + 1); # float(2)
var_dump(1.0 + 1.0); # float(2)
Substitute 1 for $a and 1.0 for $b, and you're back to the example I
originally wrote. Note that this is true even for non-commutative operators
like exponentiation:
var_dump(2 ** 3); # int(8)
var_dump(2 ** 3.0); # float(8)
var_dump(2.0 ** 3); # float(8)
var_dump(2.0 ** 3.0); # float(8)
My impression is what people consider "good" use of operator overloading is
much closer to "make things act like built in numerics" than "make
operators with fancy syntax", so some form of symmetry is necessary I think.
Regards,
Rowan Tommins
[IMSoP]
On Thu, 9 Apr 2020 at 13:18 (and subsequent correction), Dan Ackroyd <
Danack@basereality.com> wrote:$a = new A;
$b = new B;
var_dump($b + $a); # calls B::__add($b, $a); OK
var_dump($a + $b); # calls A::__add($a, $b), which is a TypeError... that code does have a TypeError. It is
calling '__add' and passing a parameter to the method that the code
can't handle.It appears to be the same error case as:
class A { public function add(A $rhs) {...} } class B { public function add(A|B $rhs) {...} } $a = new A; $b = new B; $b->add($a); // Ok $a->add($b); // TypeError
As with so much else on this topic, it depends how you think about operator
overloading - and I think that's why it's so hard to agree on an
implementation.It seems that you're picturing the overloaded + as like a normal method but
with special syntax, so that $a + $b means the same as $a->add($b) and only
that. In that interpretation, it's perfectly reasonable to have the
operation succeed or fail based only on the left-hand operand, because
that's how we're used to method dispatch working.But if you look at how "normal" operators work, it's far less obvious that
the order of operands should play any role in that decision. For instance,
when mixing float and int, the result is a float if either of the
operands is a float:var_dump(1 + 1); # int(2)
var_dump(1 + 1.0); # float(2)
var_dump(1.0 + 1); # float(2)
var_dump(1.0 + 1.0); # float(2)Substitute 1 for $a and 1.0 for $b, and you're back to the example I
originally wrote. Note that this is true even for non-commutative operators
like exponentiation:var_dump(2 ** 3); # int(8)
var_dump(2 ** 3.0); # float(8)
var_dump(2.0 ** 3); # float(8)
var_dump(2.0 ** 3.0); # float(8)My impression is what people consider "good" use of operator overloading is
much closer to "make things act like built in numerics" than "make
operators with fancy syntax", so some form of symmetry is necessary I think.Regards,
Rowan Tommins
[IMSoP]
Idle, possibly naive thought:
When applying operator overloading to objects, perhaps we could simplify matters by insisting that it only work for directly compatible types? That is:
class Foo {
public function __add(Foo $b): Foo {
return new FooOrChildOfFood();
}
}
It would work for interfaces too, but the point is that you can't operate on just any old other value... only one that is of the same type.
You could technically have a BarInterface, and then only the left-side object gets called, but it means it has to be combining two BarInterface objects, which means it knows how, because BarInterface has the necessary methods. If not, then your BarInterface is wrong and you should feel bad.
This creates a narrower use case, but perhaps one that still fits the practical usage patterns? (Eg, adding Money objects together, etc.)
If an operator by design wants different types on each side (not for numeric behavior, but for, eg, a function concatenation operator), then you would have to type the RHS you expect (eg, a callable in that case).
It's not as extensible, but the extensibility seems like it's the problem in the first place. I generally agree with those who have said that any such functionality needs to leverage the type system effectively, not side-step it.
As I said, possibly naive thought, but could deliberately reducing the scope make life simpler?
--Larry Garfield
Am 27.03.2020 um 21:10 schrieb Johannes Schlüter:
However I don't like this design.
[...]
Changed my vote back to "no" based on Johannes' arguments.
Hi Johannes,
Johannes Schlüter wrote:
I believe the pre-requisit is having some form of function overloading,
where operator functions for specific argument types can be defined. In
https://news-web.php.net/php.internals/108425 Andrea created an idea,
which is probably "ugly" but has less usage restrictions. I think
spending time on function overloading (I believe, without proving it,
this can be done with very little cost for non-overlaoded cases - by
adding a flag "overloaded" along visibility flags and check that along
with the visibility check, only in case of an overload to the
"expensive" check, which still is cheaper done in the engine than
if/else chains in userspace) and then take up operator overloading
again, rather than this smart but limited approach. (For whoever does
that: spend time in C++ and its function resolution rules incl. ADL,
not to copy, but to learn)
Thinking about what I suggested there again now, I think it's a shame
this RFC includes the following restriction:
The argument must not specify any argument typehints (an error is
thrown otherwise), as typehints and occuring type errors would break
operator evaluation (see discussion).
With strict evaluation of union types, it would be possible to achieve
something like what I suggested in a less ugly form:
class A {
/* … */
public function __add(int|float|B $other) {
/* … */
}
}
class B {
/* … */
public function __add(int|C $other) {
/* … */
}
}
From the union typs in this example, the interpreter could see, without
having to execute either overloaded method, that A provides an overload
for B, int and float, and B provides an overload for C and int, and so
it knows the same method can be called for both $a + $b
and $b + $a
,
likewise for $b + $c
and $c + $b
.
Of course, this requires interpreting type declarations in an unusual
way, and it also needs a solution to non-commutative operators (perhaps
a second parameter that would contain a boolean specifying whether
$this
is on the left), but I think it is less likely to be chaotic
than the current proposal.
Thanks,
Andrea