[PHP-DEV] [DISCUSSION] Class Constant Enums?

Hi,

Is there any interest in having enums as class constants?

I'm often finding cases where I would like to have an enum inside of a
class, but don't want a free-floating enum that's basically like
another class.

When dealing with state, it's nice to have a human readable const to
represent that state, but I always feel like they should be grouped
together.

For example:

class SSHClient {

   public const COMMAND_RESULT_SUCCESS = 0;
   public const COMMAND_RESULT_FAILURE = 1;
   public const COMMAND_RESULT_UNKNOWN = 2;
   public const COMMAND_RESULT_TIMEOUT = 3;

   // ...

}

These constants would make sense as an enum, but they make no sense
outside of the SSHClient class that uses them.

It seems that enums would be useful as class constants. There's a lot
of cases where a class implements a state machine and needs statuses,
but those status flags should be local to the class, not shared between
classes.

Example:

class SSHClient {

   public const enum CommandResult
   {
       case Success;
       case Failure;
       case Unknown;
       case Timeout;
   }

   // ...
}

// Usage:

SSHClient::CommandResult::Success

On Aug 15, 2024, at 9:37 PM, Nick Lockheart <lists@ageofdream.com> wrote:

Hi,

Is there any interest in having enums as class constants?

I’m often finding cases where I would like to have an enum inside of a
class, but don’t want a free-floating enum that’s basically like
another class.

When dealing with state, it’s nice to have a human readable const to
represent that state, but I always feel like they should be grouped
together.

Yes, I would very much like that.

-Mike
P.S. See related: https://externals.io/message/107995

Hi Nick,

Is there any interest in having enums as class constants?

I’m often finding cases where I would like to have an enum inside of a
class, but don’t want a free-floating enum that’s basically like
another class.

……

class SSHClient {

public const enum CommandResult
{
case Success;
case Failure;
case Unknown;
case Timeout;
}

// …
}

// Usage:

SSHClient::CommandResult::Success

I feel this topic could be maybe more broad and be called “nested classes” that are already supported in multiple languages: Java, Swift, Python, C#, C++, JavaScript, etc.

The syntax you showed is usually identical with what other languages use, except that probably the const is unnecessary.
The nested class can have visibility as sometimes having it private makes sense.
Accessing it through :: is probably fine, but a deeper look at the grammar might be necessary.
The nested class would have access to parent class private properties and methods.

I also mentioned this topic on the subject of defining a type in an autoloader compatible way.
And indeed, a type could also be defined nested in a class if we want to support that as well.

Now, this feature is not simple, and I think it needs proper sponsorship from someone experienced with internals.

Regards,
Alex

On Fri, Aug 16, 2024, at 6:35 AM, Alexandru Pătrănescu wrote:

Hi Nick,

Is there any interest in having enums as class constants?

I'm often finding cases where I would like to have an enum inside of a
class, but don't want a free-floating enum that's basically like
another class.

...<snip>...

class SSHClient {

   public const enum CommandResult
   {
       case Success;
       case Failure;
       case Unknown;
       case Timeout;
   }

   // ...
}

// Usage:

SSHClient::CommandResult::Success

I feel this topic could be maybe more broad and be called "nested
classes" that are already supported in multiple languages: Java, Swift,
Python, C#, C++, JavaScript, etc.

The syntax you showed is usually identical with what other languages
use, except that probably the const is unnecessary.
The nested class can have visibility as sometimes having it private
makes sense.
Accessing it through `::` is probably fine, but a deeper look at the
grammar might be necessary.
The nested class would have access to parent class private properties
and methods.

I also mentioned this topic on the subject of defining a type in an
autoloader compatible way.
And indeed, a type could also be defined nested in a class if we want
to support that as well.

Now, this feature is not simple, and I think it needs proper
sponsorship from someone experienced with internals.

Regards,
Alex

I agree with Alexandru. Since enums are 90% syntactic sugar over classes, "inner enums" would be 80% of the way to "inner classes". And I would be in favor of inner classes. :slight_smile: There's a lot of potential benefits there, but also a lot of edge cases to sort out regarding visibility, what is allowed to extend from what, etc. But that would support inner enums as well.

Based on our sibling languages (Java, Kotlin, C#, etc.), the syntax would likely be something like:

class Outer
{
  private string $foo;

  public function __construct(protected Sort $order) {}

  enum Sort
  {
    case Asc;
    case Desc;
  }

  class Inner
  {
    public function __construct(private string $baz) {}
  }

  private class HIdden
  {
    public function __construct(private string $baz) {}
  }
}

Which enables:

$case = Outer::Sort::Asc;
$o = new Outer($case);

$i = new Outer::Inner('beep');

$h = new Outer::Hidden('beep'); // Visibility error

I would have to research to see if other languages did this, but one option would be to allow an inner class to extend an outer class even if it's final, which would essentially give us sealed classes for free:

final class Outer
{
  class InnerA extends Outer {}

  class InnerB extends Outer {}

  class InnerC extends Outer {}
}

// But this is still not OK:
class Sibling extends Outer {}

Note: I have no idea how difficult/complicated this would be, but I would be in favor of exploring it.

--Larry Garfield

On Fri, Aug 16, 2024, at 16:17, Larry Garfield wrote:

On Fri, Aug 16, 2024, at 6:35 AM, Alexandru Pătrănescu wrote:

Hi Nick,

Is there any interest in having enums as class constants?

I’m often finding cases where I would like to have an enum inside of a

class, but don’t want a free-floating enum that’s basically like

another class.

……

class SSHClient {

public const enum CommandResult

{

case Success;

case Failure;

case Unknown;

case Timeout;

}

// …

}

// Usage:

SSHClient::CommandResult::Success

I feel this topic could be maybe more broad and be called "nested

classes" that are already supported in multiple languages: Java, Swift,

Python, C#, C++, JavaScript, etc.

The syntax you showed is usually identical with what other languages

use, except that probably the const is unnecessary.

The nested class can have visibility as sometimes having it private

makes sense.

Accessing it through :: is probably fine, but a deeper look at the

grammar might be necessary.

The nested class would have access to parent class private properties

and methods.

I also mentioned this topic on the subject of defining a type in an

autoloader compatible way.

And indeed, a type could also be defined nested in a class if we want

to support that as well.

Now, this feature is not simple, and I think it needs proper

sponsorship from someone experienced with internals.

Regards,

Alex

I agree with Alexandru. Since enums are 90% syntactic sugar over classes, “inner enums” would be 80% of the way to “inner classes”. And I would be in favor of inner classes. :slight_smile: There’s a lot of potential benefits there, but also a lot of edge cases to sort out regarding visibility, what is allowed to extend from what, etc. But that would support inner enums as well.

From recently looking into this for totally unrelated reasons, nested enums would be far easier to implement on a grammar level. Enums also have some constraints that make it simpler than the general “nested classes,” such as rules regarding inheritance.

As for the actual implementation, it’ll be the edges that kill you.

I would recommend just doing enums, and keep the scope smaller.

On Aug 16, 2024, at 10:17, Larry Garfield <larry@garfieldtech.com> wrote:

I would have to research to see if other languages did this, but one option would be to allow an inner class to extend an outer class even if it's final, which would essentially give us sealed classes for free:

final class Outer
{
class InnerA extends Outer {}
class InnerB extends Outer {}
class InnerC extends Outer {}
}

// But this is still not OK:
class Sibling extends Outer {}

Note: I have no idea how difficult/complicated this would be, but I would be in favor of exploring it.

--Larry Garfield

Swift allows nested classes to extend their parent. It does not allow nested classes to extend a final parent. Visibility modifiers can be applied and work as expected. Basically, types also function as the equivalent of a namespace. More broadly, any type can be nested in any other type, so you could certainly do something weird like:

class A {
	enum E {
		class C: A {}
		
		case a(A)
		case c(C)
	}
}

let e = A.E.c(A.E.C())

However, you could implement your sealed class example by having a private constructor:

class Parent {
	final class Child : Parent {
		override init() { //within lexical scope of Parent, so can see Parent's private members
			super.init()
		}
	}
	
	private init() {}
}

let parent = Parent() //'Parent' initializer is inaccessible due to 'private' protection level
let child = Parent.Child() //ok

Swift also has a fileprivate visibility, so the same could be accomplished with a fileprivate init on Parent with a non-nested Child in the same file.

-John

As a heavy Enum users, I’d love this.

Other said about going all the way as embedded classes, but I would voice the opinion that it’s gonna feature creep like crazy and probably has a lot more work in it than anyone imagines at the moment - just look on how long it took work out aviz. Enums are very limited as classes go, so it would be probably far easier to implement them. If implementation is done in a way where it’s easy to expand in the future, I don’t see a need to delay for a bigger scope.

That being said, I do have to ask about having methods and being able to implement an interface and add traits to the embedded enum?
All of my enums implement this interface

<?php
declare(strict_types=1);
namespace App\Interface;

use BackedEnum;
use Symfony\Contracts\Translation\TranslatableInterface;

interface TranslatableEnumInterface extends TranslatableInterface, BackedEnum {}

and subsequently look like this:

<?php
declare(strict_types=1);
namespace App\Enum;

use App\Interface\TranslatableEnumInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

enum ChannelTypeEnum: int implements TranslatableEnumInterface
{
case EMAIL = 1;
case WHATSAPP = 2;
case BOTH = 3;

public function trans(TranslatorInterface $translator, string $locale = null): string
{
return match ($this) {
self::EMAIL => $translator->trans('Email', locale: $locale),
self::WHATSAPP => $translator->trans('Whatsapp', domain: 'non_translatable', locale: $locale),
self::BOTH => $translator->trans('Both', locale: $locale),
};
}
}

Honestly, I would really like to do this right there in class embedded, as having a separate file really is overkill :slight_smile:

···

Arvīds Godjuks+371 26 851 664
arvids.godjuks@gmail.com
Telegram: @psihius https://t.me/psihius

Hello Larry,
I feel obliged to remind about the 80/20 rule where the last 20% of progress ends up being 80% of all the work. And from the discussion it’s already looking like there are some major questions and caveats and engine problems that are gonna rear their ugly heads. I’m more in favour starting with somewhat self-contained features and steadily work to expand on them as people put the effort into it. The same as was done with the type system. You can lay the proper foundation now, so it’s not blocking future expansion, but I really do not think full embedded classes are gonna be a short endeavor - probably multiple years if not half a decade going by prior record on features of this size.

···

Arvīds Godjuks+371 26 851 664
arvids.godjuks@gmail.com
Telegram: @psihius https://t.me/psihius

On Fri, Aug 16, 2024, at 1:31 PM, Arvids Godjuks wrote:

Hello Larry,
I feel obliged to remind about the 80/20 rule where the last 20% of
progress ends up being 80% of all the work. And from the discussion
it's already looking like there are some major questions and caveats
and engine problems that are gonna rear their ugly heads. I'm more in
favour starting with somewhat self-contained features and steadily work
to expand on them as people put the effort into it. The same as was
done with the type system. You can lay the proper foundation now, so
it's not blocking future expansion, but I really do not think full
embedded classes are gonna be a short endeavor - probably multiple
years if not half a decade going by prior record on features of this
size.
--

Arvīds Godjuks
+371 26 851 664
arvids.godjuks@gmail.com
Telegram: @psihius Telegram: Contact @psihius

To go off on this tangent for a bit...

There's a difficult balancing act to be had here. On the one hand, yes, design small and grow as you get feedback makes sense. On the other hand, if you don't think through the whole plan, those early steps could end up being blockers for expansion, not a benefit. The most obvious example, readonly was billed as a building block toward aviz. It ended up making aviz harder, and it not passing the first time around. It remains to be seen if first-class callables end up getting in the way of full PFA in the future. The piecemeal way in which anonymous functions have been added over time has led to a lot of rough edges, and made discussions about smoothing them over harder (eg, the auto-closure-long-callable RFC).

When those first steps are effectively set in stone forever, rather than something you can adjust based on future feedback, "YAGNI" becomes an actively harmful approach to system design.

--Larry Garfield