[IDEA] allow extending enum

php.internals

Rokas Šleinius

3 years ago
Enums were a very useful addition to PHP, however one aspect of them is neither explicitly documented - or seemingly even talked about. Enums were implemented as final so they cannot be extended nor can extend anything else. From a user perspective it's surprising - and actually limiting. USAGE EXAMPLE: I am making an error management system: each error presented to the user must have a unique code visible. ```php class SystemError { public function __construct( private string $errorText, private ErrorCode $code ) { } public function __toString(): { return $this->errorText . ' ' . $this->code->toString(); } } // ... enum ErrorCode { case Code_1; case Code_2; public function toString(): string { return 'Error code:' . substr($this->name, strlen('Code_')); } } ``` Now I want to modify it to support different modules with different namespaces for errors, e.g. an ApiError, simple enough: ```php enum BaseErrorCode { // ... } enum ErrorCode extends BaseErrorCode { case Code_1; case Code_2; // ... } enum ApiErrorCode extends BaseErrorCode { // ... function toString(): string { return 'Error code:API-' . substr($this->name, strlen('Code_')); } } ``` This results in a syntax error. PROPOSAL: Enums should be able to extend other enums. For a complete wishlist, add: * abstract enums; * enums allowed to implement interfaces; However since I have no experience in PHP source code, I can only provide the test suite for a possible PR this might have :( Do you think this is likely to get implemented?

Marco Pivetta

3 years ago
The point of an ENUM is precisely to restrict options within a certain range. Assume you declared `ErrorCode` with `SOMETHING_BROKE` and `PEBKAC` as possible enumeration values. If you can (somewhere, anywhere in the code) check `instanceof ErrorCode`, then you know it can only be `SOMETHING_BROKE` and `PEBKAC`, and you can write logic around it accordingly. This restriction/assumption is what makes ENUMs powerful: expanding pre-existing enums would eliminate this very core design principle, since you may now no longer have an `ErrorCode`, but some child class designed somewhere that may not even be under your control. Greets, Marco Pivetta https://mastodon.social/@ocramius https://ocramius.github.io/ On Wed, 29 Mar 2023 at 10:31, Rokas Šleinius <raveren@gmail.com> wrote:

Robert Landers

3 years ago
On Wed, Mar 29, 2023 at 10:31 AM Rokas Šleinius <raveren@gmail.com> wrote:
> > Enums were a very useful addition to PHP, however one aspect of them is neither > explicitly documented - or seemingly even talked about. > > Enums were implemented as final so they cannot be extended nor can extend > anything else. > > From a user perspective it's surprising - and actually limiting. > > USAGE EXAMPLE: > I am making an error management system: each error presented to the user > must have a unique code visible. > > ```php > class SystemError > { > public function __construct( > private string $errorText, > private ErrorCode $code > ) { > } > > public function __toString(): > { > return $this->errorText . ' ' . $this->code->toString(); > } > } > // ... > > enum ErrorCode > { > case Code_1; > case Code_2; > > public function toString(): string > { > return 'Error code:' . substr($this->name, strlen('Code_')); > } > } > ``` > > Now I want to modify it to support different modules with different > namespaces for > errors, e.g. an ApiError, simple enough: > > ```php > enum BaseErrorCode > { > // ... > } > > enum ErrorCode extends BaseErrorCode > { > case Code_1; > case Code_2; > > // ... > } > > enum ApiErrorCode extends BaseErrorCode { > // ... > function toString(): string > { > return 'Error code:API-' . substr($this->name, strlen('Code_')); > } > } > ``` > > This results in a syntax error. > > PROPOSAL: > > Enums should be able to extend other enums. > > For a complete wishlist, add: > * abstract enums; > * enums allowed to implement interfaces; > > However since I have no experience in PHP source code, I can only > provide the test suite for a possible PR this might have :( > > Do you think this is likely to get implemented? > > -- > PHP Internals - PHP Runtime Development Mailing List > To unsubscribe, visit: https://www.php.net/unsub.php >
Hey Rokas, My approach has been to use an intersection type like: BackedEnum&MyCustomBehaviorInterface This works for me but it'd be interesting if something like that wouldn't work for you.

Rokas Šleinius

3 years ago
> to restrict options within a certain range.
Okay that's an aspect of enums that I never gave much thought to, but you're completely right. However to be explicit about that aspect of functionality one would then have to also be allowed to define final enums. That way you still have both options - to restrict the option set, but to also allow adding custom ones while benefiting from other enum advantages. Thus, rfc idea seems to be: * enums should be able to extend other enums; * abstract enums; * enums allowed to implement interfaces; * final enums (NEW!)
> BackedEnum&MyCustomBehaviorInt
That's a fascinating approach but to a different problem than illustrated in OP. Would you mind sharing (in private preferably to not distract from the discussion) the gist of the problem this solves? I'm really curious, as I've yet to use intersect types :)! On Wed, 29 Mar 2023 at 11:47, Robert Landers <landers.robert@gmail.com> wrote:

Marco Pivetta

3 years ago
Hey Rokas, The idea of declaring an enum `abstract` is potentially worth exploring (I'm not convinced, but go for it), but beware that removing the implicit `final` from ENUMs is a BC break, since it breaks the assumption that an ENUM is a sealed type. If you design a proposal now, you need to consider that `final` as the implicit default is an existing contract that really enables ENUM usage. Marco Pivetta https://mastodon.social/@ocramius https://ocramius.github.io/ On Wed, 29 Mar 2023 at 11:05, Rokas Šleinius <raveren@gmail.com> wrote:

Rokas Šleinius

3 years ago
> BC break
Hm, do you really think so? I wouldn't say removing the final attribute from enums actually "breaks" any functionality. It WOULD allow some potentially illicit hacking to the end users of our code, that's true. I'd say a minor possibility of impact. And anyway, isn't that what got you to fall in love with PHP in the first place? Some good ol' illicit hacking of some dude's library :) On Wed, 29 Mar 2023 at 12:22, Marco Pivetta <ocramius@gmail.com> wrote:

Sebastian Bergmann

3 years ago
Am 29.03.2023 um 11:31 schrieb Rokas Šleinius:
> I wouldn't say removing the final attribute from enums actually "breaks" any functionality.
I am with Marco on this: removing the "finality" from enum would be a major backward compatiblity break as it breaks a fundamental assumption about enums.

Tim Düsterhus

3 years ago
Hi On 3/29/23 11:42, Sebastian Bergmann wrote:
> Am 29.03.2023 um 11:31 schrieb Rokas Šleinius: >> I wouldn't say removing the final attribute from enums actually "breaks" any functionality. > > I am with Marco on this: removing the "finality" from enum would be a > major backward compatiblity break as it breaks a fundamental assumption > about enums. >
And to give a specific example from PHP 8.3: As part of the Randomizer additions RFC (https://wiki.php.net/rfc/randomizer_additions), PHP 8.3 got its first "natively included" enum (\Random\IntervalBoundary). This enum works together with the new Randomizer::getFloat() method to specify if the given min and max value may be returned or not. The Randomizer::getFloat() method internally includes switch statement that chooses the implementation based on the IntervalBoundary value given. If a user would be able to extend the IntervalBoundary enum, the method would not be able to make sense of it. The IntervalBoundary enum completely enumerates all four possible combinations for the two possible states for each of the two boundaries (2*2 = 4). By definition there is no other valid value. Best regards Tim Düsterhus

Christian Schneider

3 years ago
Am 29.03.2023 um 11:55 schrieb Tim Düsterhus <tim@bastelstu.be>:
> On 3/29/23 11:42, Sebastian Bergmann wrote: >> Am 29.03.2023 um 11:31 schrieb Rokas Šleinius: >>> I wouldn't say removing the final attribute from enums actually "breaks" any functionality. >> I am with Marco on this: removing the "finality" from enum would be a >> major backward compatiblity break as it breaks a fundamental assumption >> about enums. > > And to give a specific example from PHP 8.3: As part of the Randomizer additions RFC (https://wiki.php.net/rfc/randomizer_additions), PHP 8.3 got its first "natively included" enum (\Random\IntervalBoundary). This enum works together with the new Randomizer::getFloat() method to specify if the given min and max value may be returned or not. > > The Randomizer::getFloat() method internally includes switch statement that chooses the implementation based on the IntervalBoundary value given. > > If a user would be able to extend the IntervalBoundary enum, the method would not be able to make sense of it. > > The IntervalBoundary enum completely enumerates all four possible combinations for the two possible states for each of the two boundaries (2*2 = 4). By definition there is no other valid value.
I do understand the reason behind making Enums final and your example illustrates the point very well. I still have a question I already asked in a different context: If there ever is the need to add a case to an Enum, was there any thought put into making this possible? Or is this categorically ruled out when using Enums? Let’s look at a very, very hypothetical example: Imagine a Randomizer is much, much slower for certain boundaries and it is decided that some programs do not care about Closed/Open but instead care more about speed. So they would want to use something like IntervalBoundary::Fastest. As far as I understand such an addition would *never* be possible, right? This means people defining Enums have to be very very certain that no one will ever want another value, right? Regards, - Chris

Rokas Šleinius

3 years ago
Hi! There's a hypothetical (based on a similar real world problem that I am facing) example for the use of extending enums in the OP. And I don't suppose Tim was arguing for not allowing enum extendability, but rather contributing a real world example where users who were having some totally legal fun hacking around with internal PHP code would break said internal PHP code. Which may or may not be a big deal from my uneducated point of view, just saying :) And all could be prevented just by adding a `final` statement to the definition of `IntervalBoundary`. On Wed, 29 Mar 2023 at 13:42, Christian Schneider <cschneid@cschneid.com> wrote:

Tim Düsterhus

3 years ago
Hi On 3/29/23 13:01, Rokas Šleinius wrote:
> There's a hypothetical (based on a similar real world problem that I > am facing) example for the use of extending enums in the OP. > > And I don't suppose Tim was arguing for not allowing enum > extendability, but rather contributing a real world example where > users who were having some totally legal fun hacking around with > internal PHP code would break said internal PHP code.
I'm seeing that you already thought about the topic more based on the other responses. But for the record: I *was* arguing for not allowing enum extendability.
> Which may or may not be a big deal from my uneducated point of view, > just saying :) And all could be prevented just by adding a `final` > statement to the definition of `IntervalBoundary`. >
Requiring folks to add 'final' to their existing enums for them to behave as expected would be considered a breaking change. Best regards Tim Düsterhus

Tim Düsterhus

3 years ago
Hi On 3/29/23 12:41, Christian Schneider wrote:
>> And to give a specific example from PHP 8.3: As part of the Randomizer additions RFC (https://wiki.php.net/rfc/randomizer_additions), PHP 8.3 got its first "natively included" enum (\Random\IntervalBoundary). This enum works together with the new Randomizer::getFloat() method to specify if the given min and max value may be returned or not. >> >> The Randomizer::getFloat() method internally includes switch statement that chooses the implementation based on the IntervalBoundary value given. >> >> If a user would be able to extend the IntervalBoundary enum, the method would not be able to make sense of it. >> >> The IntervalBoundary enum completely enumerates all four possible combinations for the two possible states for each of the two boundaries (2*2 = 4). By definition there is no other valid value. > > I do understand the reason behind making Enums final and your example illustrates the point very well. > > I still have a question I already asked in a different context: If there ever is the need to add a case to an Enum, was there any thought put into making this possible? Or is this categorically ruled out when using Enums?
Adding a case to an enum would certainly be considered a breaking change for folks that rely on having exhaustively handled all possible cases.
> Let’s look at a very, very hypothetical example: Imagine a Randomizer is much, much slower for certain boundaries and it is decided that some programs do not care about Closed/Open but instead care more about speed. So they would want to use something like IntervalBoundary::Fastest.
In this specific hypothetical example, the fastest case would be one of the existing 4 cases - an alias if you want to call it that. Defining an alias is possible without breaking compatibility by leveraging class constants: <?php enum IntervalBoundary { case ClosedOpen; case ClosedClosed; case OpenClosed; case OpenOpen; public const Fastest = self::ClosedOpen; } var_dump(IntervalBoundary::ClosedOpen === IntervalBoundary::Fastest); function foo(IntervalBoundary $b) { var_dump($b); } foo(IntervalBoundary::Fastest); -> https://3v4l.org/tT0Ho Alternatively a method "public static function getFastest(): self" would be possible.
> As far as I understand such an addition would *never* be possible, right? This means people defining Enums have to be very very certain that no one will ever want another value, right? >
I believe the short answer to the last question is "Yes". Best regards Tim Düsterhus

Rokas Šleinius

3 years ago
Thank you for all the insight, I feel like having extendable enums is an agreeable and organic way forward, so if I may, I would like to go ahead and create an RFC draft for this and keep adding the discussion outcomes till it settles for good. If I can get the Karma to do so, of course, my wiki name is raveren, thank you! Sidenote, just asking :) was this expected behaviour that I had to analyse PHP source of the Docuwiki, create a new input field in the registration form named "spam" and fill it to gain a wiki username? Otherwise it would just keep expecting a different answer from me no matter what email I put in... R. On Wed, 29 Mar 2023 at 11:31, Rokas Šleinius <raveren@gmail.com> wrote:

Niklas Keller

3 years ago
Am Mi., 29. März 2023 um 12:49 Uhr schrieb Rokas Šleinius <raveren@gmail.com
>:
> Sidenote, just asking :) was this expected behaviour that I had to > analyse PHP source of the Docuwiki, create a new input field in > the registration form named "spam" and fill it to gain a wiki > username? Otherwise it would just keep expecting a different > answer from me no matter what email I put in... >
No, thanks for reporting. This should be fixed now. Best, Niklas

Girgias

3 years ago
On Wed, 29 Mar 2023 at 09:31, Rokas Šleinius <raveren@gmail.com> wrote:
> Enums were a very useful addition to PHP, however one aspect of them is > neither > explicitly documented - or seemingly even talked about. > > Enums were implemented as final so they cannot be extended nor can extend > anything else. >
This is by design. Enumerations are in type theory parlance sum types. Objects in PHP, in general, are what are called product types.
> From a user perspective it's surprising - and actually limiting. >
The point of enums is to be limiting.
> USAGE EXAMPLE: > I am making an error management system: each error presented to the user > must have a unique code visible. > > ```php > class SystemError > { > public function __construct( > private string $errorText, > private ErrorCode $code > ) { > } > > public function __toString(): > { > return $this->errorText . ' ' . $this->code->toString(); > } > } > // ... > > enum ErrorCode > { > case Code_1; > case Code_2; > > public function toString(): string > { > return 'Error code:' . substr($this->name, strlen('Code_')); > } > } > ``` > > Now I want to modify it to support different modules with different > namespaces for > errors, e.g. an ApiError, simple enough: > > ```php > enum BaseErrorCode > { > // ... > } > > enum ErrorCode extends BaseErrorCode > { > case Code_1; > case Code_2; > > // ... > } > > enum ApiErrorCode extends BaseErrorCode { > // ... > function toString(): string > { > return 'Error code:API-' . substr($this->name, strlen('Code_')); > } > } > ``` > > This results in a syntax error. >
You are approaching the design of this in a fundamentally wrong way. Enums, as sum types, are meant to enclose a *finite* number of states. If you want to expand those finite number of states, the solution is to use a union type. Let's say you have those 3 enums: enum GenericErrors { case ErrorType1; case ErrorType2; } enum FileErrors { case FileErrorType1; case FileErrorType2; case FileErrorType3; } enum NetworkErrors { case NetworkErrorType1; case NetworkErrorType2; case NetworkErrorType3; } And you have a function that can fail with either a generic error or a file error then you can be explicit by having: function foo(): T|GenericErrors|FileErrors { /* Do something or Fail */ } This signature provides you with great type safety and can be checked via static analysis that *all* error cases are handled. Moreover, you know you don't need to handle NetworkErrors. Extending an Enum *loses* you this type safety, because now ff you say something returns a GenericErrors well who knows if you cover all cases because someone might have ad-hock added a new one. PHP's type system is not perfect yet, but it has become powerful enough that you can do things like what I just described. Ideally we would have generics, and you could use a Result/Either Monad (fancy word for "wrapper") to have a return type of Result<T, GenericErrors|FileErrors> Use the type system properly instead of trying to shove everything into an "Inheritance" view of things. Union types are not "bad", they are another way of creating a sum type. (You could, with a lot of effort, create "proper" enumerations since PHP 8.0 by using union types and singleton classes.) PROPOSAL:
> > Enums should be able to extend other enums. > > For a complete wishlist, add: > * abstract enums; >
* enums allowed to implement interfaces;
>
Enums can already implement interfaces: https://3v4l.org/tKtQ0
> However since I have no experience in PHP source code, I can only > provide the test suite for a possible PR this might have :( > > Do you think this is likely to get implemented? >
I dear hope so not. Breaking fundamental type theoretical assumptions is terrible. Best regards, George P. Banyard

Rowan Collins

3 years ago
On Wed, 29 Mar 2023 at 13:30, G. P. B. <george.banyard@gmail.com> wrote:
> On Wed, 29 Mar 2023 at 09:31, Rokas Šleinius <raveren@gmail.com> wrote: > > Enums were implemented as final so they cannot be extended nor can extend > > anything else. > > > > This is by design. > Enumerations are in type theory parlance sum types. > Objects in PHP, in general, are what are called product types. >
I was just about to write a response along the same lines. I think the confusion comes because in Java-style OOP, the keyword "extends" actually does two things at once: 1. Takes the existing definition and adds to or over-rides parts of it 2. Takes the existing type and declares a sub-type What you actually want to do with an enum is: 1. Take the existing definition and add cases to it 2. Take the existing type and declare a *super-type* To give a concrete example of why, consider Christian's hypothetical:
> Imagine a Randomizer is much, much slower for certain boundaries and it
is decided that some programs do not care about Closed/Open but instead care more about speed. So they would want to use something like IntervalBoundary::Fastest. If we use the normal definition of "extends", we could say this: enum PerfIntervalBoundary extends IntervalBoundary { case Fastest; } Now we have an enum with three cases, which is what we wanted. But we've also declared a *sub-type*: we've said that "every PerfIntervalBoundary is-a IntervalBoundary". That means that anywhere that previously expected an IntervalBoundary, we can pass PerfIntervalBoundary::Fastest - but none of the existing Randomizer classes will know what to do with it! What we actually wanted to do is leave all the existing uses of IntervalBoundary alone, but create a new WeirdPerfRandomizer which accepts the new type. We wanted to list "PerfIntervalBoundary" as the input type, but still be able to pass it IntervalBoundary::Closed. In short, we want to say "every IntervalBoundary is-a PerfIntervalBoundary" - the relationship is the other way around. As George says, this can be implemented right now by using a union type constraint, like PerfIntervalBoundary|IntervalBoundary, which means "allow any case of PerfIntervalBoundary as well as any value of IntervalBoundary". If we wanted to support it more directly, we would need a new keyword to make clear that this is *not* a sub-type relationship - perhaps something like "expands", "allows", or "encompasses". Regards,
-- Rowan Tommins [IMSoP]

Rokas Šleinius

3 years ago
Ok so I am trying to find the argumentation here:
>This is by design. >The point of enums is to be limiting.
This is clearly an assumption. That statement is not in the docs or RFC, nor such an oversimplification makes logical sense to me, but maybe you have a source to back it up..?
>Re: problem in the OP >You are approaching the design of this in a fundamentally wrong way.
The described problem is dealing with error *codes* not *types*, which means that each error is assigned a new *code*. We are dealing with anticipated errors here, things like validation, data integrity, 3rd party API breakage etc. e.g.: user gets code 456 trying to ship out package, it says package not paid, user says he looked it up, its paid. The developer has a very concrete lead to the problem - lookup usages of `ErrorCodes::Code_456`. The set in question is *finite*, as you say of course, but much larger and different in principle than in the solution you are proposing. Problem in OP is looking for a way for end users to keep adding new domains to the error codes passed to the parent system to be handled. The actual, real requirements are: 1. Ease of management of codes: simplicity of adding *new* codes to the set, easy lookup, simplicity of making sure a code is not used elsewhere before using it. All perfectly fulfilled by enums. 2. Reusing the parent system for different modules, using the parent system in two different packages I want to opensource. Open Sourcing the parent system.. Rowan Tommins is right on the target however, that is amazing feedback for the desired functionality, excellent distillment of logic and all correct points! I will take some time to think about that. On Wed, 29 Mar 2023 at 15:30, G. P. B. <george.banyard@gmail.com> wrote:

Rowan Collins

3 years ago
On Wed, 29 Mar 2023 at 14:22, Rokas Šleinius <raveren@gmail.com> wrote:
> Ok so I am trying to find the argumentation here: > > >This is by design. > >The point of enums is to be limiting. > > This is clearly an assumption. That statement is not in the docs or > RFC, nor such an oversimplification makes logical sense to me, but > maybe you have a source to back it up..? >
From a theory point of view, *any* type definition is about limiting allowed values. The difference between "mixed" and "integer" is that "mixed" allows both 'Hello' and 42, but "integer" does not - it defines a tighter limit. In the same way, saying "I accept an error code" means "I accept an error code *and nothing else*" - the definition of what is and what isn't an "error code" is deliberately a *limit* on the values that you accept.
> > >Re: problem in the OP > >You are approaching the design of this in a fundamentally wrong way. > > The described problem is dealing with error *codes* not *types*
An enum is a type, so that's why George was talking about types. He was making the same point, in different words, as I did: the relationship between types implied by the keyword "extends" is not the relationship you want in this case.
> The set in question is *finite*, as you say of course, but much larger > and different in principle than in the solution you are proposing. > Problem in OP is looking for a way for end users to keep adding new > domains to the error codes passed to the parent system to be handled. >
This is the key point - if you pass an error code that didn't previously exist, the existing system *won't* be able to handle it. That's why enums are inherently restrictive: the system wants to be able to say "this is the list of error codes I understand, don't give me anything else". If you have a system that accepts the new codes as well as the old ones, you can use a union type declaration as George says: enum AdditionalErrorCode { case Code_456; } class ExtendedErrorHandler extends StandardErrorHandler { public function handle(StandardErrorCode|AdditionalErrorCode $code) { switch ( $code ) { case AdditionalErrorCode::Code_456: // handling for new code goes here break; default: // assert($code instanceof StandardErrorCode); parent::handle($code); break; } } } Regards,
-- Rowan Tommins [IMSoP]

Rokas Šleinius

3 years ago
First, I'm pretty sure I agree now that enums should *not* be `extend`-ed as regular classes, there's a fundamental difference as you state - the extended enum is a fundamentally different THING as it can hold different values - when an enum's sole purpose is to hold one of the set of values. That is to say - for most implementations that's the desired case. I'll refer to the solution to the OP problem as ECS, Error Code System. For ECS, the most elegant, but also the only, solution I can come up with - is the kind of enum that you also suggest, a `base-enum`. `abstract enum`? Let me try again to describe the problem that I came here with that is unsolvable by union types: 1. ECS is a *generic* component. I want to plug it into any working system (like an existing cms). 2. Invoking it must provide a unique ErrorCode. The existing cms has to create an enum that will be accepted as `type: ErrorCode`. It cannot edit the existing ErrorCode as it is part of ECS and not the cms. 3. There is NO special handling for the custom ErrorCode's that the users of ECS create. We are just using the enums inferred value. I will keep processing this situation further, but for now it seems to me like 1. Extending enums is fundamentally flawed. 2. Basing enums off of other enums has valid usage scenarios. In that case, and, again, this needs way more thought to it, it's not such a "generic way forward" that seemed to me at first and might only provide marginal value at the cost of type complexity and that is most probably, unfortunately, not worth it... (unless `abstract enum` might make sense, but my brain needs some time off of this problem for now) Thank you for such a thought out discussion, everyone! On Wed, 29 Mar 2023 at 16:56, Rowan Tommins <rowan.collins@gmail.com> wrote:

Rowan Collins

3 years ago
On Wed, 29 Mar 2023 at 15:26, Rokas Šleinius <rokas.sleinius@gmail.com> wrote:
> Invoking it must provide a unique ErrorCode.
The concept of "uniqueness" only makes sense within a certain context - the CMS easily create these two enums: enum HttpErrorCode: int extends ErrorCode { case NotFound = 404; } enum SQLErrorCode: int extends ErrorCode { case BadColumnRef = 404; } If ECS accepts both HttpErrorCode::NotFound and SQLErrorCode::BadColumnRef, and looks at the integer value, they will both be 404. Are they still "unique" in any useful way? The existing cms has
> to create an enum that will be accepted as `type: ErrorCode`. > [...] > There is NO special handling for the custom ErrorCode's that the > users of ECS create.
I don't understand this pair of requirements: if ECS doesn't have handling based on the value, why does it care what type it is? If it cares that it can turn it into an integer, it can just request an integer directly, or an object implementing an appropriate interface: interface ErrorValueInterface { public function getCode(): int; } From the description given, it seems like enums are sinmply the wrong tool for the job. Regards,
-- Rowan Tommins [IMSoP]

Larry Garfield

3 years ago
On Wed, Mar 29, 2023, at 2:25 PM, Rokas Šleinius wrote:
> First, I'm pretty sure I agree now that enums should *not* be > `extend`-ed as regular classes, there's a fundamental difference as > you state - the extended enum is a fundamentally different THING as it > can hold different values - when an enum's sole purpose is to hold one > of the set of values. > > That is to say - for most implementations that's the desired case. > > I'll refer to the solution to the OP problem as ECS, Error Code System. > > For ECS, the most elegant, but also the only, solution I can come up > with - is the kind of enum that you also suggest, a `base-enum`. > `abstract enum`? > > Let me try again to describe the problem that I came here with that is > unsolvable by union types: > > 1. ECS is a *generic* component. I want to plug it into any working > system (like an existing cms). > 2. Invoking it must provide a unique ErrorCode. The existing cms has > to create an enum that will be accepted as `type: ErrorCode`. It > cannot edit the existing ErrorCode as it is part of ECS and not the > cms. > 3. There is NO special handling for the custom ErrorCode's that the > users of ECS create. We are just using the enums inferred value. > > I will keep processing this situation further, but for now it seems to me like > > 1. Extending enums is fundamentally flawed. > 2. Basing enums off of other enums has valid usage scenarios. > > In that case, and, again, this needs way more thought to it, it's not > such a "generic way forward" that seemed to me at first and might only > provide marginal value at the cost of type complexity and that is most > probably, unfortunately, not worth it... > > (unless `abstract enum` might make sense, but my brain needs some time > off of this problem for now) > > Thank you for such a thought out discussion, everyone!
1) Please don't top-post. 2) Some good reading material for this topic, both on enums and on error handling: https://peakd.com/hive-168588/@crell/on-the-use-of-enums https://peakd.com/hive-168588/@crell/much-ado-about-null They're not short, but that's because they are complete. :-) 3) In the error code case, the answer is to leverage the fact that enums *do* support interfaces. interface ErrorCode { public function message(): string; } enum CommonErrors: string implements ErrorCode { case YourFault = 'You screwed up'; case OurFault = 'We screwed up'; public function message(): string { return $this->value; } } readonly class SomeoneElseError implements ErrorCode { public function __construct(privateUser $atFault) {} public function message(): string { return sprintf('%s screwed up', $this->atFault->name()); } } Now an error handling system can type against the ErrorCode interface, common errors have trivially simple enum values you can use, but you can also make your own. In this case, you're *not* using an enum as a limited-set; you're taking advantage of it being a way to predefine singleton objects. I would consider this a completely valid use of enums, and actually do something very similar in my serialization library: https://github.com/Crell/Serde/blob/master/src/Renaming/RenamingStrategy.php https://github.com/Crell/Serde/blob/master/src/Renaming/Cases.php https://github.com/Crell/Serde/blob/master/src/Renaming/Prefix.php 4) As others have said, extending enums is a bad idea with lots of reasons it's a bad idea, both conceptual and pragmatic. However, I would be open to discussing a `use` for enums to import cases from one enum into another. There's two concerns with that to consider: A) What happens to methods on the imported enum? Are they pulled in as well? Do interface definitions carry over? I don't know. B) The long-term goal is to expand Enums to include associated values (https://wiki.php.net/rfc/tagged_unions, although that's a bit out of date now so don't take it as a roadmap). How would `use`-ing an enum within another enum affect that? I have no idea, off hand. --Larry Garfield

Rokas Šleinius

3 years ago
On Wed, 29 Mar 2023 at 18:40, Larry Garfield <larry@garfieldtech.com> wrote:
> > On Wed, Mar 29, 2023, at 2:25 PM, Rokas Šleinius wrote: > > First, I'm pretty sure I agree now that enums should *not* be > > `extend`-ed as regular classes, there's a fundamental difference as > > you state - the extended enum is a fundamentally different THING as it > > can hold different values - when an enum's sole purpose is to hold one > > of the set of values. > > > > That is to say - for most implementations that's the desired case. > > > > I'll refer to the solution to the OP problem as ECS, Error Code System. > > > > For ECS, the most elegant, but also the only, solution I can come up > > with - is the kind of enum that you also suggest, a `base-enum`. > > `abstract enum`? > > > > Let me try again to describe the problem that I came here with that is > > unsolvable by union types: > > > > 1. ECS is a *generic* component. I want to plug it into any working > > system (like an existing cms). > > 2. Invoking it must provide a unique ErrorCode. The existing cms has > > to create an enum that will be accepted as `type: ErrorCode`. It > > cannot edit the existing ErrorCode as it is part of ECS and not the > > cms. > > 3. There is NO special handling for the custom ErrorCode's that the > > users of ECS create. We are just using the enums inferred value. > > > > I will keep processing this situation further, but for now it seems to me like > > > > 1. Extending enums is fundamentally flawed. > > 2. Basing enums off of other enums has valid usage scenarios. > > > > In that case, and, again, this needs way more thought to it, it's not > > such a "generic way forward" that seemed to me at first and might only > > provide marginal value at the cost of type complexity and that is most > > probably, unfortunately, not worth it... > > > > (unless `abstract enum` might make sense, but my brain needs some time > > off of this problem for now) > > > > Thank you for such a thought out discussion, everyone! > > 1) Please don't top-post. > > 2) Some good reading material for this topic, both on enums and on error handling: > > https://peakd.com/hive-168588/@crell/on-the-use-of-enums > https://peakd.com/hive-168588/@crell/much-ado-about-null > > They're not short, but that's because they are complete. :-) > > 3) In the error code case, the answer is to leverage the fact that enums *do* support interfaces. > > interface ErrorCode > { > public function message(): string; > } > > enum CommonErrors: string implements ErrorCode > { > case YourFault = 'You screwed up'; > case OurFault = 'We screwed up'; > > public function message(): string > { > return $this->value; > } > } > > readonly class SomeoneElseError implements ErrorCode > { > public function __construct(privateUser $atFault) {} > > public function message(): string > { > return sprintf('%s screwed up', $this->atFault->name()); > } > } > > Now an error handling system can type against the ErrorCode interface, common errors have trivially simple enum values you can use, but you can also make your own. In this case, you're *not* using an enum as a limited-set; you're taking advantage of it being a way to predefine singleton objects. I would consider this a completely valid use of enums, and actually do something very similar in my serialization library: > > https://github.com/Crell/Serde/blob/master/src/Renaming/RenamingStrategy.php > https://github.com/Crell/Serde/blob/master/src/Renaming/Cases.php > https://github.com/Crell/Serde/blob/master/src/Renaming/Prefix.php > > 4) As others have said, extending enums is a bad idea with lots of reasons it's a bad idea, both conceptual and pragmatic. However, I would be open to discussing a `use` for enums to import cases from one enum into another. > > There's two concerns with that to consider: > > A) What happens to methods on the imported enum? Are they pulled in as well? Do interface definitions carry over? I don't know. > > B) The long-term goal is to expand Enums to include associated values (https://wiki.php.net/rfc/tagged_unions, although that's a bit out of date now so don't take it as a roadmap). How would `use`-ing an enum within another enum affect that? I have no idea, off hand. > > --Larry Garfield
> 1) Please don't top-post.
Am I doing it right now? I've never been on a mailing list. In fact, I was too young and missed out on BBS as well, and now using this laughably archaic collaboration method, receiving mostly stone-harsh replies generously sprinkled with genius insights, having to quite literally hack a registration form as one of the several required steps for contribution... Wow this is a really remarkable experience for me :D
> 2) Some good reading material for this topic, both on enums and on error handling:
Thanks, noted, bookmarked, subscribed. BTW the ECS example is only dealing with Errors because it's a concept I thought people would grasp and relate to easily.
> yesssss, enums support interfaces!!!!!!
Less than five minutes away from the keyboard I realised that this is the key and my problem is solved more elegantly than the initial idea of extending - the `interface` is perfect, thank you to everyone who pointed it out! I am really impressed how elegant and actually genius the enum implementation is. So well rounded, minimal, yet creates so much value! Re: Rowan:
> The concept of "uniqueness" only makes sense within a certain context
That's why in ECS example I added the `toString()` method to the enum, but you're right, I should have named it something along the lines of `getErrorPrefix()` - and now - using the interface approach I can assure any enum passed to "AddError" will also provide a prefix along with the enum->name (error code) + enum->value (error text) :)
> `type ApiError = DatabaseError|HttpError;`
Sounds cool and useful, hope there's motivation enough to see it through, but that's a separate topic, I'm glad this discussion happened, bye now everyone!!

Tim Düsterhus

3 years ago
Hi On 3/30/23 09:36, Rokas Šleinius wrote:
>> 1) Please don't top-post. > > Am I doing it right now? I've never been on a mailing list.
Yes. Ideally you would also cut the quoted parts to a minimum (like I'm doing here), but I believe the Gmail web interface doesn't make this easy.
> to quite literally hack a registration form as one of the several > required steps for contribution... Wow this is a really remarkable > experience for me :D
It appears that the wiki registration got broken during a wiki upgrade a few days ago. Unfortunate timing on your end. I've let the responsible person know about this. Best regards Tim Düsterhus

Lynn

3 years ago
On Thu, Mar 30, 2023 at 10:04 AM Tim Düsterhus <tim@bastelstu.be> wrote:
> Hi > > On 3/30/23 09:36, Rokas Šleinius wrote: > >> 1) Please don't top-post. > > > > Am I doing it right now? I've never been on a mailing list. > > Yes. Ideally you would also cut the quoted parts to a minimum (like I'm > doing here), but I believe the Gmail web interface doesn't make this easy. >
Heya, I'm using the Gmail web interface and I think I haven't really encountered major problems so far. Open the [...] below and you can split and type underneath quoted parts. That said, mailing lists are ancient and in my opinion a horrible way of doing things. I've had my fair share of issues to sign up initially, there's a weird etiquette of how you have to structure your replies, and most importantly you have to remind yourself every time to press "reply all". There are also issues with messages ending up in junk-mail or caught by spam-filters, and you heavily depend on your mail client to properly group messages so you can actually digest them in a conversational form. Email is also lacking edit buttons and integrated code blocks like we're used to with github (or markdown in general). I don't know how other projects deal with this though.

Christian Schneider

3 years ago
Am 30.03.2023 um 10:50 schrieb Lynn <kjarli@gmail.com>:
> That said, mailing lists are ancient and in my opinion a horrible way of doing things.
Mailing lists have been around for a long time, yes, but it also means the way we use it for larger scale communication has been refined over time. Please consider that this process is based on a lot of experience.
> I've had my fair share of issues to sign up initially, there's a weird etiquette of how you have to structure your replies
Once you familiarized yourself with the notion of giving enough context to your reply parts (but not too much) it becomes a very valuable way of keeping communication efficient. The main point is that 1 person is writing a reply but 100s to 1000s have to read it and they do not have the same mental context as the writer. So spending this extra effort to minimize the effort of the reader is well worth it.
> , and most importantly you have to remind yourself.every time to press "reply all".
As it is sufficient to just reply to the mailing list this could technically be changed by adding a Reply-To. The downside would be that it makes private replies harder and the occasional slip-up could send a private reply to the list instead. IMHO private replies should be kept to a minimum so I think (re-)considering adding a Reply-To might be worth it.
> There are also issues with messages ending up in junk-mail or caught by spam-filters, and you heavily depend on your > mail client to properly group messages so you can actually digest them in a conversational form.
Threading emails has been part of mail clients for 20 years, I'm puzzled that people still consider this to be an issue. Mail filters also allow to automatically separate into folders, if one wishes so.
> Email is also lacking edit buttons
This is actually an important feature! It forces the writer to be more careful with replies which in turn safes time for the 100s of readers. It also means that I don't miss edits or have to go back to edited messages and guess what has changed. Editing messages just doesn't scale well with large recipient groups IMHO.
> and integrated code blocks like we're used to with github (or markdown in general).
I don't think it is hard to format example code in a way that they are readable in an email. Again: It is a little bit of extra work for the writer but this tends to lead to simpler code examples for the reader. Regards, - Chris

Dan Ackroyd

3 years ago
Hi Rokas, On Thu, 30 Mar 2023 at 08:36, Rokas Šleinius <rokas.sleinius@gmail.com> wrote:
> > On Wed, 29 Mar 2023 at 18:40, Larry Garfield <larry@garfieldtech.com> wrote: > > > > 1) Please don't top-post. > > Am I doing it right now? I've never been on a mailing list.
You're replying in the preferred manner, yes. Quoting only the needed amount and putting replies underneath. Though deleting all non-relevant text is also good. Have two possibly useful guidelines: * Mailing list etiquette guidelines - https://phpopendocs.com/internals/mailing_list * Email etiquette for people new to newsgroups / PHP internals - https://phpopendocs.com/internals/mailing_list_for_younguns which might get copied across to php.net at some point...
> In fact, I was too young and missed out on BBS as well, and now using > this laughably archaic collaboration method, receiving mostly > stone-harsh replies generously sprinkled with genius insights, having > to quite literally hack a registration form as one of the several > required steps for contribution... Wow this is a really remarkable > experience for me :D
Well, it builds character. And as soon as someone suggests something that is obviously better, and not just replacing one set of problems with another, the discussions might move.
> I am really impressed how elegant and actually genius the enum > implementation is. So well rounded, minimal, yet creates so much > value!
And it only took the PHP project about 20 years of discussion to get an agreeable implementation... cheers Dan Ack

Davey Shafik

3 years ago
> On Mar 29, 2023, at 06:56, Rowan Tommins <rowan.collins@gmail.com> wrote: > > On Wed, 29 Mar 2023 at 14:22, Rokas Šleinius <raveren@gmail.com> wrote: > >> Ok so I am trying to find the argumentation here: >>> This is by design. >>> The point of enums is to be limiting. >> This is clearly an assumption. That statement is not in the docs or >> RFC, nor such an oversimplification makes logical sense to me, but >> maybe you have a source to back it up..? > > > From a theory point of view, *any* type definition is about limiting > allowed values. The difference between "mixed" and "integer" is that > "mixed" allows both 'Hello' and 42, but "integer" does not - it defines a > tighter limit. > > In the same way, saying "I accept an error code" means "I accept an error > code *and nothing else*" - the definition of what is and what isn't an > "error code" is deliberately a *limit* on the values that you accept. > > > >>> Re: problem in the OP >>> You are approaching the design of this in a fundamentally wrong way. >> The described problem is dealing with error *codes* not *types* > > > > An enum is a type, so that's why George was talking about types. He was > making the same point, in different words, as I did: the relationship > between types implied by the keyword "extends" is not the relationship you > want in this case. > > > >> The set in question is *finite*, as you say of course, but much larger >> and different in principle than in the solution you are proposing. >> Problem in OP is looking for a way for end users to keep adding new >> domains to the error codes passed to the parent system to be handled. > > > This is the key point - if you pass an error code that didn't previously > exist, the existing system *won't* be able to handle it. > > That's why enums are inherently restrictive: the system wants to be able to > say "this is the list of error codes I understand, don't give me anything > else". > > If you have a system that accepts the new codes as well as the old ones, > you can use a union type declaration as George says: > > enum AdditionalErrorCode { > case Code_456; > } > > class ExtendedErrorHandler extends StandardErrorHandler { > public function handle(StandardErrorCode|AdditionalErrorCode $code) { > switch ( $code ) { > case AdditionalErrorCode::Code_456: > // handling for new code goes here > break; > default: > // assert($code instanceof StandardErrorCode); > parent::handle($code); > break; > } > } > }
The issue of contention here is that inheritance isn't the correct mechanism for reusing enum definitions to expand on them. What about allowing "use" syntax like traits? enum HttpErrors { case HTTP_STATUS_400; case HTTP_STATUS_401; … } enum ApiErrors { use HttpErrors; case JsonParserError; case GraphQLParserError; } Now you have ApiErrors::HTTP_STATUS_400 etc without ApiErrors being is-a HttpErrors. - Davey

Mark Baker

3 years ago
On 29/03/2023 16:31, Davey Shafik wrote:
>> On Mar 29, 2023, at 06:56, Rowan Tommins <rowan.collins@gmail.com> wrote: >> >> On Wed, 29 Mar 2023 at 14:22, Rokas Šleinius <raveren@gmail.com> wrote: >> >>> Ok so I am trying to find the argumentation here: >>>> This is by design. >>>> The point of enums is to be limiting. >>> This is clearly an assumption. That statement is not in the docs or >>> RFC, nor such an oversimplification makes logical sense to me, but >>> maybe you have a source to back it up..? >> >> From a theory point of view, *any* type definition is about limiting >> allowed values. The difference between "mixed" and "integer" is that >> "mixed" allows both 'Hello' and 42, but "integer" does not - it defines a >> tighter limit. >> >> In the same way, saying "I accept an error code" means "I accept an error >> code *and nothing else*" - the definition of what is and what isn't an >> "error code" is deliberately a *limit* on the values that you accept. >> >> >> >>>> Re: problem in the OP >>>> You are approaching the design of this in a fundamentally wrong way. >>> The described problem is dealing with error *codes* not *types* >> >> >> An enum is a type, so that's why George was talking about types. He was >> making the same point, in different words, as I did: the relationship >> between types implied by the keyword "extends" is not the relationship you >> want in this case. >> >> >> >>> The set in question is *finite*, as you say of course, but much larger >>> and different in principle than in the solution you are proposing. >>> Problem in OP is looking for a way for end users to keep adding new >>> domains to the error codes passed to the parent system to be handled. >> >> This is the key point - if you pass an error code that didn't previously >> exist, the existing system *won't* be able to handle it. >> >> That's why enums are inherently restrictive: the system wants to be able to >> say "this is the list of error codes I understand, don't give me anything >> else". >> >> If you have a system that accepts the new codes as well as the old ones, >> you can use a union type declaration as George says: >> >> enum AdditionalErrorCode { >> case Code_456; >> } >> >> class ExtendedErrorHandler extends StandardErrorHandler { >> public function handle(StandardErrorCode|AdditionalErrorCode $code) { >> switch ( $code ) { >> case AdditionalErrorCode::Code_456: >> // handling for new code goes here >> break; >> default: >> // assert($code instanceof StandardErrorCode); >> parent::handle($code); >> break; >> } >> } >> } > The issue of contention here is that inheritance isn't the correct mechanism for reusing enum definitions to expand on them. What about allowing "use" syntax like traits? > > enum HttpErrors { > case HTTP_STATUS_400; > case HTTP_STATUS_401; > … > } > > enum ApiErrors { > use HttpErrors; > > case JsonParserError; > case GraphQLParserError; > } > > Now you have ApiErrors::HTTP_STATUS_400 etc without ApiErrors being is-a HttpErrors.
That would make a lot of sense, and I was just having similar thoughts myself. ApiErrors isn't the same enum, or an extension of HttpErrors: it's a new Enum, but it does include all HttpErrors cases (and methods) without the need to duplicate code, and any new cases that are subsequently added to HttpErrors will automatically be incorporated in ApiErrors.
-- Mark Baker

Mark Baker

3 years ago
On 29/03/2023 16:43, Mark Baker wrote:
> On 29/03/2023 16:31, Davey Shafik wrote: >>> On Mar 29, 2023, at 06:56, Rowan Tommins <rowan.collins@gmail.com> >>> wrote: >>> >>> On Wed, 29 Mar 2023 at 14:22, Rokas Šleinius <raveren@gmail.com> >>> wrote: >>> >>>> Ok so I am trying to find the argumentation here: >>>>> This is by design. >>>>> The point of enums is to be limiting. >>>> This is clearly an assumption. That statement is not in the docs or >>>> RFC, nor such an oversimplification makes logical sense to me, but >>>> maybe you have a source to back it up..? >>> >>>  From a theory point of view, *any* type definition is about limiting >>> allowed values. The difference between "mixed" and "integer" is that >>> "mixed" allows both 'Hello' and 42, but "integer" does not - it >>> defines a >>> tighter limit. >>> >>> In the same way, saying "I accept an error code" means "I accept an >>> error >>> code *and nothing else*" - the definition of what is and what isn't an >>> "error code" is deliberately a *limit* on the values that you accept. >>> >>> >>> >>>>> Re: problem in the OP >>>>> You are approaching the design of this in a fundamentally wrong way. >>>> The described problem is dealing with error *codes* not *types* >>> >>> >>> An enum is a type, so that's why George was talking about types. He was >>> making the same point, in different words, as I did: the relationship >>> between types implied by the keyword "extends" is not the >>> relationship you >>> want in this case. >>> >>> >>> >>>> The set in question is *finite*, as you say of course, but much larger >>>> and different in principle than in the solution you are proposing. >>>> Problem in OP is looking for a way for end users to keep adding new >>>> domains to the error codes passed to the parent system to be handled. >>> >>> This is the key point - if you pass an error code that didn't >>> previously >>> exist, the existing system *won't* be able to handle it. >>> >>> That's why enums are inherently restrictive: the system wants to be >>> able to >>> say "this is the list of error codes I understand, don't give me >>> anything >>> else". >>> >>> If you have a system that accepts the new codes as well as the old >>> ones, >>> you can use a union type declaration as George says: >>> >>> enum AdditionalErrorCode { >>>    case Code_456; >>> } >>> >>> class ExtendedErrorHandler extends StandardErrorHandler { >>>    public function handle(StandardErrorCode|AdditionalErrorCode >>> $code) { >>>         switch ( $code ) { >>>             case AdditionalErrorCode::Code_456: >>>                // handling for new code goes here >>>             break; >>>             default: >>>                 // assert($code instanceof StandardErrorCode); >>>                 parent::handle($code); >>>             break; >>>        } >>>    } >>> } >> The issue of contention here is that inheritance isn't the correct >> mechanism for reusing enum definitions to expand on them. What about >> allowing "use" syntax  like traits? >> >> enum HttpErrors { >>     case HTTP_STATUS_400; >>     case HTTP_STATUS_401; >>      … >> } >> >> enum ApiErrors { >>     use HttpErrors; >> >>     case JsonParserError; >>     case GraphQLParserError; >> } >> >> Now you have ApiErrors::HTTP_STATUS_400 etc without ApiErrors being >> is-a HttpErrors. > > That would make a lot of sense, and I was just having similar thoughts > myself. > > ApiErrors isn't the same enum, or an extension of HttpErrors: it's a > new Enum, but it does include all HttpErrors cases (and methods) > without the need to duplicate code, and any new cases that are > subsequently added to HttpErrors will automatically be incorporated in > ApiErrors. >
Another benefit of this approach would be to allow the inclusion of multiple enums, in the same way that we can include multiple traits. enum HttpErrors {     case HTTP_STATUS_400;     case HTTP_STATUS_401;      … } enum DatabaseErrors {     case RECORD_NOT_FOUND;     case DATABASE_NOT_AVAILABLE;      … } enum ApiErrors {     use HttpErrors, DatabaseErrors;     case JsonParserError;     case GraphQLParserError; }
-- Mark Baker

Larry Garfield

3 years ago
On Wed, Mar 29, 2023, at 4:28 PM, Mark Baker wrote:
> Another benefit of this approach would be to allow the inclusion of > multiple enums, in the same way that we can include multiple traits. > > > enum HttpErrors { > >     case HTTP_STATUS_400; > >     case HTTP_STATUS_401; > >      … > > } > > enum DatabaseErrors { > >     case RECORD_NOT_FOUND; >     case DATABASE_NOT_AVAILABLE; > >      … > > } > > > > enum ApiErrors { > >     use HttpErrors, DatabaseErrors; > > > >     case JsonParserError; > >     case GraphQLParserError; > > }
All that said, we could get the same functionality with fewer questions if we just finally got around to introducing type definitions. type ApiError = DatabaseError|HttpError; function handleErrors(ApiError $error) {...} That would obviate the need for a lot of workarounds. --Larry Garfield

Tim Düsterhus

3 years ago
Hi On 3/29/23 16:31, Davey Shafik wrote:
> The issue of contention here is that inheritance isn't the correct mechanism for reusing enum definitions to expand on them. What about allowing "use" syntax like traits? > > enum HttpErrors { > case HTTP_STATUS_400; > case HTTP_STATUS_401; > … > }
I understand that this is just an example, but I'd like to note that using an enum for HTTP status codes would likely be a misuse. Valid HTTP status codes are all three-digit numbers in 1xx to 5xx and HTTP error codes are everything 4xx and 5xx. HTTP status code are specifically designed for you to be able to handle them generically based on the first digit, even if you don't know about the specific code.
> enum ApiErrors { > use HttpErrors; > > case JsonParserError; > case GraphQLParserError; > } > > Now you have ApiErrors::HTTP_STATUS_400 etc without ApiErrors being is-a HttpErrors.
Including all the cases of another enum is not unlikely to result in conflicts, because the enum name effectively acts as a namespace with regard to the enum cases. Staying with the HttpErrors enum for sake of the example, you'd rather use `case Forbidden` instead of `case HTTP_STATUS_403`, because `HttpErrors::HTTP_STATUS_403` redundantly includes the 'HTTP'. Mark's example of DatabaseError would possibly also have a `case Forbidden` in case your database password is incorrect or in case you may not access the queried table or whatever. Instead it would likely be preferable to get tagged unions (https://wiki.php.net/rfc/tagged_unions) and then nest the errors, like commonly done in Rust: enum ApiErrors { case HttpRequest(HttpErrors $httpError); case JsonParsing; case GraphQlParsing; } And then `$foo = ApiErrors::HttpRequest(HttpErrors::Forbidden);`. The Exception equivalent would be leveraging the `$previous` value to provide the context. Best regards Tim Düsterhus

Ilija Tovilo

3 years ago
Hi Davey On 29.03.23 16:31, Davey Shafik wrote:
> The issue of contention here is that inheritance isn't the correct mechanism for reusing enum definitions to expand on them. What about allowing "use" syntax like traits? > > enum HttpErrors { > case HTTP_STATUS_400; > case HTTP_STATUS_401; > … > } > > enum ApiErrors { > use HttpErrors; > > case JsonParserError; > case GraphQLParserError; > } > > Now you have ApiErrors::HTTP_STATUS_400 etc without ApiErrors being is-a HttpErrors.
This would still require making ApiErrors a supertype of HttpErrors so that a parameter typed as ApiErrors accepts a HttpErrors::HTTP_STATUS_400 value. This essentially mimicks implicit multiple inheritance. The fact that these are implicit (for HttpErrors, that is) makes this quite confusing, and difficult to implement technically (usually classes are "locked" after linking). I would much prefer the previously suggested approaches (using algebraic data types, a.k.a. tagged unions, or union types of two enums). Ilija