On Wed, 29 Mar 2023 at 09:31, Rokas Šleinius <rave...@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

Reply via email to