RFC proposal: values getter in BackedEnum

php.internals

Sergii Shymko

3 years ago
Hi, I'd like to propose an improvement to backed enumerations introduced in PHP 8.1. When using enums, it's very common to get all values using the following boilerplate: $enumValues = array_map( fn (\BackedEnum $case) => $case->value, ExampleEnum::cases() ); The primary use case is when an enum is used in a "set" (array of values). For example, a user inputs a subset of available values. The input may be optional defaulting to all values if omitted. For example, an array option of a CLI tool: $inputValues = $input->getOption('enum') ?: $enumValues The secondary use case is for informational purposes. For example, the CLI tool may list all available values of an enum option in --help. The list of values can also be used to produce informative error messages: $output->writeln("Option is invalid, allowed values: " . implode(', ', $enumValues)) The last use case is input validation. Although tryFrom() can be used, having access to all values can come in handy. For instance, you could immediately get valid and invalid enum values: $invalidValues = array_diff($inputValues, $enumValues); The implementation would be more verbose and wasteful otherwise: $invalidValues = array_filter($inputValues, fn ($value) => !ExampleEnum::tryFrom($value)); The proposal is to add the values getter to backed enums: interface BackedEnum extends UnitEnum { public static values(): array; } $enumValues = ExampleEnum::values(); Indeed, it's possible to implement this userland in a trait reused across enums. However, the implementation in C will be substantially more performant. For instance, the values() method call can resolve to a constant! An alternative syntax is a pseudo-constant, for example, ExampleEnum::values. Unlike the constant, the method would be part of the interface complementing cases(). Method cases() itself is not a constant only because it returns instances. I'm fine with either syntax leaning towards the method for consistency. Please let me know your thoughts. With enough support, I'd be happy to submit an RFC and implement it myself. Some guidance on the performance optimization aspect may be necessary though. I'm still missing the karma necessary to create RFCs in the wiki. Regards, Sergii Shymko

Larry Garfield

3 years ago
On Wed, Feb 8, 2023, at 5:30 PM, Sergii Shymko wrote:
> Hi, > > I'd like to propose an improvement to backed enumerations introduced in > PHP 8.1. > When using enums, it's very common to get all values using the > following boilerplate: > $enumValues = array_map( > fn (\BackedEnum $case) => $case->value, > ExampleEnum::cases() > ); > > The primary use case is when an enum is used in a "set" (array of values). > For example, a user inputs a subset of available values. > The input may be optional defaulting to all values if omitted. > For example, an array option of a CLI tool: > $inputValues = $input->getOption('enum') ?: $enumValues > > The secondary use case is for informational purposes. > For example, the CLI tool may list all available values of an enum > option in --help. > The list of values can also be used to produce informative error > messages: > $output->writeln("Option is invalid, allowed values: " . implode(', ', > $enumValues)) > > The last use case is input validation. > Although tryFrom() can be used, having access to all values can come in > handy. > For instance, you could immediately get valid and invalid enum values: > $invalidValues = array_diff($inputValues, $enumValues); > The implementation would be more verbose and wasteful otherwise: > $invalidValues = array_filter($inputValues, fn ($value) => > !ExampleEnum::tryFrom($value)); > > The proposal is to add the values getter to backed enums: > interface BackedEnum extends UnitEnum > { > public static values(): array; > } > $enumValues = ExampleEnum::values(); > > Indeed, it's possible to implement this userland in a trait reused across enums. > However, the implementation in C will be substantially more performant. > For instance, the values() method call can resolve to a constant! > > An alternative syntax is a pseudo-constant, for example, > ExampleEnum::values. > Unlike the constant, the method would be part of the interface > complementing cases(). > Method cases() itself is not a constant only because it returns > instances. > I'm fine with either syntax leaning towards the method for consistency. > > Please let me know your thoughts. > With enough support, I'd be happy to submit an RFC and implement it myself. > Some guidance on the performance optimization aspect may be necessary though. > I'm still missing the karma necessary to create RFCs in the wiki. > > Regards, > Sergii Shymko
I would be OK with this, assuming the method approach. (The class constant feels clunkier.) --Larry Garfield

Rowan Collins

3 years ago
On Thu, 9 Feb 2023 at 01:30, Sergii Shymko <sergey@shymko.net> wrote:
> I'd like to propose an improvement to backed enumerations introduced in > PHP 8.1. > When using enums, it's very common to get all values using the following > boilerplate: > $enumValues = array_map( > fn (\BackedEnum $case) => $case->value, > ExampleEnum::cases() > ); >
Since the value is exposed as a public property, there is already a much shorter way of writing this: $enumValues = array_column(BackedEnum::cases(), 'value'); As I pointed out in a StackOverflow answer [ https://stackoverflow.com/a/71235974/157957], you can get the case name the same way, and use different arguments to array_column to get combinations like a look up table from value to name: $nameToValue = array_column(BackedEnum::cases(), 'name', 'value'); Regards,
-- Rowan Tommins [IMSoP]

Sergii Shymko

3 years ago
________________________________ From: Rowan Tommins <rowan.collins@gmail.com> Sent: Thursday, February 9, 2023 12:56 AM To: internals@lists.php.net <internals@lists.php.net> Subject: Re: [PHP-DEV] RFC proposal: values getter in BackedEnum On Thu, 9 Feb 2023 at 01:30, Sergii Shymko <sergey@shymko.net> wrote:
> I'd like to propose an improvement to backed enumerations introduced in > PHP 8.1. > When using enums, it's very common to get all values using the following > boilerplate: > $enumValues = array_map( > fn (\BackedEnum $case) => $case->value, > ExampleEnum::cases() > ); >
Since the value is exposed as a public property, there is already a much shorter way of writing this: $enumValues = array_column(BackedEnum::cases(), 'value'); As I pointed out in a StackOverflow answer [ https://stackoverflow.com/a/71235974/157957], you can get the case name the same way, and use different arguments to array_column to get combinations like a look up table from value to name: $nameToValue = array_column(BackedEnum::cases(), 'name', 'value'); Regards,
-- Rowan Tommins [IMSoP] Ho Rowan, Thanks for pointing out a shorter way of coding this via array_column()! While it's good to know and an improvement, I think, the proposal still stands. BackedEnum::values() is much more convenient than array_column(BackedEnum::cases(), 'value'); And the potential for the performance optimization to return a constant is still there. IMO, other variations of array_column() returning a map don't seem to be useful in practice. Particularly, what is knowing the case name good for? Do you propose to resolve it to the case instance like so? $case = constant(ExampleEnum::class . '::' . $caseName); My understanding is that backed enums serve the purpose of doing the mapping. Regards, Sergii

Tim Düsterhus

3 years ago
Hi On 2/9/23 17:18, Sergii Shymko wrote:
> BackedEnum::values() is much more convenient than array_column(BackedEnum::cases(), 'value');
I suspect the answer is "preference", but I have to ask: Why? Personally looking at that I find it more convenient to use an existing function that I can apply to any array or object than having to learn about a new convenience method that is a few characters shorter, but entirely specific to enums. It also raises the question why your proposal only includes BackedEnum::values() instead of also including UnitEnum::names()? I find that I seldomly have the need for backed enums in the first place. The name is sufficient to uniquely identify the case and it's also the value that is used when serialize()ing an enum. Best regards Tim Düsterhus PS: I appreciate that you follow the "no top post" rule, however I find it hard to identify the end of the quoted text and the beginning of your reply, because there is no clear separator or indicator. It also appears to break the externals.io parser [1], which is unfortunate. Is it possible for you to configure your email client to use a quote format that includes the standard '> ' prefix in quoted lines? [1] https://externals.io/message/119499#119511

Rowan Collins

3 years ago
On Thu, 9 Feb 2023 at 16:18, Sergii Shymko <sergey@shymko.net> wrote:
> > IMO, other variations of array_column() returning a map don't seem to be > useful in practice. > Particularly, what is knowing the case name good for? Do you propose to > resolve it to the case instance like so? > $case = constant(ExampleEnum::class . '::' . $caseName); > My understanding is that backed enums serve the purpose of doing the > mapping. >
The same argument can be made for your validation examples: the enum is "doing the mapping" when you use tryFrom. My opinion on this probably comes from my general opinion of backed enums - to me, the opaque case object is primary, and the "value" is just an arbitrary piece of data attached. I would have preferred to be able to attach any number of such properties, e.g. Options::verbose->longArgumentName, Options::verbose->shortArgumentName. Regards,
-- Rowan Tommins [IMSoP]