Skip to content

Userspace operator overloading #5156

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 16 commits into from

Conversation

jbtronics
Copy link

This is an draft implementation for this RFC.

@nikic suggested opening a pull request so this implementation can be discussed (see here.


if (!EG(exception) && (Z_TYPE_P(result) == IS_UNDEF || Z_TYPE_P(result) == IS_NULL))
{
zend_error(E_ERROR, "Method %s::%s must return a non-null value", ZSTR_VAL(ce->name), Z_STRVAL(fci.function_name));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use zend_type_error().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I'm wondering: Might it make sense to allow a null return value to indicate that this operand combination is not supported? In particular, I'm thinking that if you have $a * $b and A::__mul($a, $b) returns null, then B::__mul($a, $b) would be tried.

So if some library that provides A implements __mul but does not know about B, then B still has a chance to implement a __mul behavior if it knows about A.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use zend_type_error().

Thank you for the hint.

Might it make sense to allow a null return value to indicate that this operand combination is not supported?

I thought about this too. I think some mechanisms like this is highly useful, but i wonder if returning null value is the most intuitive way for that (returning null looks like the operator would return null). Maybe introducing a special Exception/Throwable, like OperandsNotSupportedException, that will be thrown in that cases and signal to use the other operands handler.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing does carry overhead, perhaps a better option is a separate ce that can only be constructed via a call to an internal constant or method?

It could then return that, instead of throwing.

`
class Foo {
public static function __add(Foo $x, $y): Foo|ArithmeticOperationUnsupported {
if (!some_condition($y)) {
return Arithmetic::OPERATION_UNSUPPORTED;
}

return new Foo(...);
}
}
`

{
case ZEND_ADD:
fcic.function_handler = ce->__add;
ZVAL_STRING(&fci.function_name, ZEND_ADD_FUNC_NAME);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually needed? If the ce->__add etc are properly pre-initialized, then it should not be necessary to set the function name here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea behind this, was to save the name of magic function called, so it can be for the usage notice below.
Maybe I should move this to the zend_error(E_NOTICE, ..) line, so the var is only created if really needed.

@jbtronics
Copy link
Author

I have added the possibility that an operator handler can signal, that it does not support the given operand types by returning the new constant PHP_OPERAND_TYPES_NOT_SUPPORTED.

The handler is also skipped if an TypeError occurs. This has the advantage that you can specify (in simple cases) what types are accepted by using typehints: So if a class can specify a handler like __add(Vector3 $lhs, Vector3 $rhs) and it automatically signals that it does not support arguments other than Vector3.

I have some question: Is there any possibility to differentiate between, if a function has returned null or returned nothing (void)? I have tried to check the type of the retval for IS_UNDEF, but the type is always null. (I have even tried to specify the called function as ZEND_USER_FUNCTION, so that zend_call_function() does set the return value to null, but that did not work. It seems that the null value is set on some other layer then).

@nikic
Copy link
Member

nikic commented Feb 9, 2020

The handler is also skipped if an TypeError occurs. This has the advantage that you can specify (in simple cases) what types are accepted by using typehints: So if a class can specify a handler like __add(Vector3 $lhs, Vector3 $rhs) and it automatically signals that it does not support arguments other than Vector3.

I don't think this is a good idea. The TypeError is not necessarily coming from the call to the operator method, it might also occur as part of the implementation, and we wouldn't want those to get lost.

I have some question: Is there any possibility to differentiate between, if a function has returned null or returned nothing (void)? I have tried to check the type of the retval for IS_UNDEF, but the type is always null. (I have even tried to specify the called function as ZEND_USER_FUNCTION, so that zend_call_function() does set the return value to null, but that did not work. It seems that the null value is set on some other layer then).

No, this is not possible. return; and return null; behave the same from the perspective of the caller.

…ggered now (we can not differentiate between void and null return value).
@jbtronics
Copy link
Author

jbtronics commented Feb 12, 2020

I don't think this is a good idea. The TypeError is not necessarily coming from the call to the operator method, it might also occur as part of the implementation, and we wouldn't want those to get lost.

Okay I agree. I have changed my implementation the way that it checks if the given operand types match the handler's signature (the logic from zend_execute is reused), before calling the operand. This way it is possible to specify the acceptable types in signature without catching TypeErrors.

What do you think about this approach?

@dakur
Copy link

dakur commented Feb 20, 2020

Why calling unimplemented operator just does not throw an exception instead of returning constant?

Copy link

@dakur dakur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Er… I did not mean to make a review actually, just want to comment the RFC. Not pretty sure what this review feature does, so I apologize if I broke some standard process by using it. 🙂

Zend/zend.h Outdated
zend_function *__sr;
zend_function *__or;
zend_function *__and;
zend_function *__xor;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My two bits for better developer experience:

I suggest to use full names of methods, this is just not readable. __add, __subtract, __multiply etc. is much clearer.

Also, on* method naming convention came to my mind (__onAddition, __onMultiplication etc.) You can think about the operator as an event emitter ($a + $b) and the magic method as a handler. Not sure if it is a good idea, but I guess it is more explicit about when the method is run actually.

@jbtronics
Copy link
Author

Why calling unimplemented operator just does not throw an exception instead of returning constant?

If the operator is not implemented or the types are not supported, an exception is thrown. But this is done by the engine and not by the user, as the engine has overview about the operator implementations on both (left and right hand) objects.

@cmb69 cmb69 added the RFC label Feb 28, 2020
@jbtronics
Copy link
Author

Fixed. Thank you for the hint.

@jbtronics
Copy link
Author

I found a strange memory leak ocurring, when you call $a++ (or $a--) on an object (see https://travis-ci.org/github/php/php-src/jobs/665462666#L714).

Interesting is that this does not happen when you call $a + 1, so I think this is not related to my code, as PHP translates $a++ to $a + 1 before even calling my handler. My guess would be that it is somehow related to the fact, that $a is overridden immediately (it get assigned the value the __add handler returns).
Any idea what could cause this leak or how to debug it better (I tested valgrind with USE_ZEND_ALLOC=0 but it did not show anything useful).

@kocsismate
Copy link
Member

Hi @jbtronics ,

I just wanted to let you know (but I didn't want to write a mail) that I really appreciate that you put so much effort into the implementation as well as the design of this RFC. The main reason why I voted no is because I don't agree with the feature itself, primarily the possible side-effects operations could have.

@NickvdMeij
Copy link

@kocsismate just out of interest, what side-effects are you talking about? Are you talking here about the user-defined side effects that might occur when they implement some weird logic themselves, or side effects under the hood which developers cannot avoid?

Was personally very excited to see this RFC, and I'm really wondering at the moment why this is being declined

@kocsismate
Copy link
Member

@NickvdMeij it's maybe because people with voting rights are narrow-minded (including me)... So I can imagine most people are not very familiar with this concept and have bad preconceptions for operator overloading, and even the fact that this RFC seems like a very detailed one can't change that. The very first RFC of my own is also failing so I know it's difficult :(

Apart from negative bias, I don't like that I'm not always sure if an object supports operator overloading or not. Also, it still feels very weird for me that you can compare DateTime objects with each other - and somehow I don't like to use this feature. I think that's all - at least from my side.

P.S. I meant the user-defined side effects. It might be some exaggeration, but I don't know.

@NickvdMeij
Copy link

@NickvdMeij it's maybe because people with voting rights are narrow-minded (including me)... So I can imagine most people are not very familiar with this concept and have bad preconceptions for operator overloading, and even the fact that this RFC seems like a very detailed one can't change that. The very first RFC of my own is also failing so I know it's difficult :(

@kocsismate I didn't mean to be negative, but I like your humor :) You guys have all the right to decline, thats the reason you guys have the voting rights in the first place.

Apart from negative bias, I don't like that I'm not always sure if an object supports operator overloading or not. Also, it still feels very weird for me that you can compare DateTime objects with each other - and somehow I don't like to use this feature. I think that's all - at least from my side.

The reason it is weird, in my opinion at least, is because it's different from other behaviours that already exist in PHP. By making it user-defined, and well documentated, you can explain this behaviour and educate people in how to use it properly. You can even use the DateTime class as an example.

P.S. I meant the user-defined side effects. It might be some exaggeration, but I don't know.

User-defined side effects, or to generalize a bit; "problems", have been around since programming has been invented. I'm sure the PHP community can handle this problem and again educate people on the use of overloading properties. But thats just my opinion ;-).

Thanks anyways for your response, was just curious about the "why" the RFC was declines. Keep on doing the good work 👏

@kocsismate
Copy link
Member

kocsismate commented Mar 24, 2020

@NickvdMeij Don't worry, I did't take your question as an offense. And I didn't mean to be sarcastic with my first sentence :) It's just that there are some things for which I'm not easy to persuade. And apparently the majority of voters think similarly about this topic.

You make a good point about the uniformity of internal and userland classes though (actually, Nikita also came up with the same argument on the internals mailing list). What I can say from my side is that I'll keep my eyes on the vote, and re-read the proposal sometime soon so that I might find more things I like about the feature. :)

@tuqqu
Copy link
Contributor

tuqqu commented Mar 25, 2020

I feel like this feature would greatly improve developer experience in (quite rare) cases where it is needed (matrix/vector libraries etc) and there is really no need to be afraid of it at all. I hope this RFC will pass.

@@ -20,10 +20,14 @@ var_dump($c);
echo "Done\n";
?>
--EXPECTF--
Notice: You have to implement the __add function in class stdClass to use this operator with an object in %sadd_002.php on line %d
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notice doesn't make sense for non-user defined classes!
I see no point in emitting it here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this message does not make sense here, but I'm fine with having a notice on object to int conversion (which is undefined behavior) here.

continue;
}

zend_error(E_NOTICE, "You have to implement the %s function in class %s to use this operator with an object",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A check here like if (ce->type != ZEND_INTERNAL_CLASS) before zend_error() would do the job for notices on user classes only.

@jbtronics
Copy link
Author

As the RFC was declined, I close this PR...

@jbtronics jbtronics closed this Apr 8, 2020
@phpia
Copy link

phpia commented Nov 25, 2021

So, this funcionality is rejected ? :(

@FlameStorm
Copy link

It's so pity to see that operators overloading STILL rejected. :(

@shanginn
Copy link

shanginn commented Jul 6, 2023

sooo I see on the RFC page, that this proposal have 38 Yes and 28 no. How is this rejected?

PS: would love to see that implemented...

@jbtronics
Copy link
Author

sooo I see on the RFC page, that this proposal have 38 Yes and 28 no. How is this rejected?

New language features (like most changes to the PHP language) need a 2/3 majority for their RFCs, which was not reached here.

@seebeen
Copy link

seebeen commented Jan 25, 2024

What is the process of reopening an RFC for another PHP version? 🤔

@tuqqu
Copy link
Contributor

tuqqu commented Jan 28, 2024

@seebeen according to the policy, a rejected RFC can be reopened after 6 months. My guess is that a new discussion on the mailing list needs to be brought up first.

@jbtronics
Copy link
Author

@seebeen according to the policy, a rejected RFC can be reopened after 6 months. My guess is that a new discussion on the mailing list needs to be brought up first.

Yes. But it would not make sense to just repropose the same RFC again. I guess without some changes it will be rejected again.

And even then. As far as I remember it, some of the voters how voted against it, were against the introduction of operator overloading in general. So even with the best implementation of operating overloading you might not be able to convince them.

So there is the question, if it is worth to invest work into proposing a new RFC.

@tuqqu
Copy link
Contributor

tuqqu commented Jan 28, 2024

People come and go or change their minds on topics. This was a rather close vote and had a positive community feedback, so it is safe to say it is not pointless to reopen the discussion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.