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