On Thu, Mar 20, 2025, at 6:02 PM, Daniel Scherzer wrote:
> On Thu, Mar 20, 2025 at 4:00 PM Larry Garfield <la...@garfieldtech.com> wrote:
>> I have a use case for this in Serde, so would be in favor.
>> 
>> We should not block this kind of improvement on the hope of generics.  Worst 
>> case, we have this plus generics so you have options, how terrible.
>> 
>
> Would you mind sharing details of your Serde use case? It seems that 
> the BackedEnum example might not have been the best (since it is for 
> static methods) and so perhaps a userland case where this would be used 
> would help.
>
> --Daniel

Simplified example to show the thing we care about:

I have an interface Formatter, like:

interface Formatter
{
    public function serializeInitialize(ClassSettings $classDef, Field 
$rootField): mixed;

    public function serializeInt(mixed $runningValue, Field $field, ?int 
$next): mixed;

    public function serializeFloat(mixed $runningValue, Field $field, ?float 
$next): mixed;

   // And other methods for other types
}

The $runningValue is of a type known concretely to a given implementation, but 
not at the interface level.  It's returned from serializeIntialize(), and then 
passed along to every method, recursively, as it writes out an object.

So for instance, the JSON formatter looks like this:

class JsonSerializer implements Formatter
{
     public function serializeInitialize(ClassSettings $classDef, Field 
$rootField): array
    {
        return ['root' => []];
    }

    /**
     * @param array<string, mixed> $runningValue
     * @return array<string, mixed>
     */
    public function serializeInt(mixed $runningValue, Field $field, ?int 
$next): array
    {
        $runningValue[$field->serializedName] = $next;
        return $runningValue;
    }
}

Because JsonFormatter works by building up an array and passing it to 
json_encode(), eventually.  So $runningValue is guaranteed to always be an 
array.  I can narrow the return value to an array, but not the parameter.

The JsonStreamFormatter, however, has a stream object that it passes around 
(which wraps a file handle internally):

class JsonStreamFormatter implements Formatter
{
    public function serializeInitialize(ClassSettings $classDef, Field 
$rootField): FormatterStream
    {
        return FormatterStream::new(fopen('php://temp/', 'wb'));
    }

   /**
     * @param FormatterStream $runningValue
     */
    public function serializeInt(mixed $runningValue, Field $field, ?int 
$next): FormatterStream
    {
        $runningValue->write((string)$next);
        return $runningValue;
    }
}

Again, I can narrow the return value but not the param.

To be clear, generics would absolutely be better in this case.  I'm just not 
holding my breath.

Associated Types would probably work in this case, too, since it's always 
relevant when creating a new concrete object, not when parameterizing a common 
object.  If we got that, I'd probably use that instead.

Changing the interface to use `never` instead of `mixed` would have the weakest 
guarantees of the three, since it doesn't force me to use the *same* widened 
type on serializeInt(), serializeFloat(), serializeString(), etc., even though 
it would always be the same.  But it would allow me to communicate more type 
information than I can now.

How compelling this use case is, I leave as an exercise for the reader.

--Larry Garfield

Reply via email to