On Fri, Dec 17, 2021 at 10:37 AM Jordan LeDoux <jordan.led...@gmail.com>
wrote:

> On Fri, Dec 17, 2021 at 9:43 AM Matt Fonda <matthewfo...@gmail.com> wrote:
>
>> Hi Jordan,
>>
>> Thanks for the RFC. I have a couple questions:
>>
>> Suppose I have classes Foo and Bar, and I want to support the following
>> operations:
>>
>> - Foo * Bar (returns Foo)
>> - Bar * Foo (returns Foo)
>>
>> If I understand correctly, there are three possible ways I could
>> implement this:
>>
>> a) Implement the * operator in Foo, accepting a Foo|Bar, and use the
>> OperandPosition to determine if I am doing Foo * Bar or Bar * Foo and
>> implement the necessary logic accordingly.
>> b) Implement the * operator in Bar, accepting a Foo|Bar, and use the
>> OperandPosition to determine if I am doing Foo * Bar or Bar * Foo and
>> implement the necessary logic accordingly.
>> c) Implement the * operator in Foo, accepting a Bar (handles Foo * Bar
>> side); Implement the * operator in Bar, accepting a Foo (handles Bar * Foo
>> side)
>>
>> Is this understanding correct? If so, which is the preferred approach and
>> why? If not, can you clarify the best way to accomplish this?
>>
>
> You are correct in your understanding. All three of these would accomplish
> what you want, but would have varying levels of maintainability. Which you
> choose would depend on the specifics of the Foo and Bar class. For
> instance, if the Bar class was one that you didn't ever expect to use on
> its own with operators, only in combination with Foo, then it would make
> sense to use option 1. The inverse would be true if Bar was the only one
> you ever expected to use with operators on its own.
>
> The better way, in general, would be for Foo and Bar to extend a common
> class that implements the overload in the *same* way for both. In most
> circumstances, (but not all), if you have two different objects used with
> each other with operators, they should probably share a parent class or be
> instances of the same class. Like I said, this isn't always true, but for
> the majority of use cases I would expect it is.
>
>
>> Next, suppose I also want to support int * Foo (returns int). To do this,
>> I must implement * in Foo, which would look like one of the following
>> (depending on which approach above)
>>
>> public operator *(Foo|int $other, OperandPos $pos): Foo|int { ... }
>> public operator *(Foo|Bar|int $other, OperandPos $pos): Foo|int { ... }
>>
>> Now, suppose I have an operation like `42 * $foo`, which as described
>> above, should return int. It seems it is not possible to enforce this via
>> typing, is that correct? i.e. every time I use this, I am forced to do:
>>
>> $result = 42 * $foo;
>> if (is_int($result)) {
>>     // can't just assume it's an int because * returns Foo|int
>> }
>>
>
> In general I would say that returning a union from an operator overload is
> a recipe for problems. I would either always return an int, or always
> return an instance of the calling class. Mostly, this is because any scalar
> can be easily represented with a class as well.
>
> Jordan
>

Hi Jordan,

Thanks for the info. I share Stas's unease with having many different
places we must look in order to understand what $foo * $bar actually
executes. I'm also uneasy with the requirement of union typing in order for
an operator to support multiple types. This will lead to implementations
which are essentially many methods packed into one: one "method" for each
type in the union, and potentially one "method" for each LHS vs. RHS. When
combined, these two issues will make readability difficult. It will be
difficult to know what $foo * $bar actually executes, and once we find it,
the implementation may be messy.

I agree that returning a union is a recipe for a problem, but the fact that
the input parameter must be a union can imply that the return value must
also be a union. For example, Num * Num may return Num, but Num * Vector3
may return Vector3, or Vector3 * Vector3 may represent dot product and
return Num. But let's not get hung up on specific scenarios; it's a problem
that exists in the general sense, and I believe that if PHP is to offer
operator overloading, it should do so in a way that is type safe and
unambiguous.

Method overloading could address both issues (LHS always "owns" the
implementation, and has a separate implementation for each type allowed on
the RHS). But I see this as a non-starter because it would not allow scalar
types on the LHS.

It's difficult to think of a solution that addresses both of these issues
without introducing more. One could imagine something like the following:

register_operator(*, function (Foo $lhs, Bar $rhs): Foo { ...});
register_operator(*, function (Bar $lhs, Foo $rhs): Foo { ...});
register_operator(*, function (int $lhs, Foo $rhs): int { ...});

But this just brings a new set of problems, including visibility issues
(i.e. can't use private fields in the implementation), and the fact that
this requires executing a function at runtime rather than being defined at
compile time.

I don't have any ideas that address all of these issues, but I do think
they deserve further thought.

Thanks,
--Matt

Reply via email to