Re: [RFC] Enumerations
| From: | Larry Garfield | Date: | Tue, 08 Dec 2020 18:14:15 +0000 |
| Subject: | Re: [RFC] Enumerations | ||
| References: | 1 2 3 4 5 6 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-112470@lists.php.net to get a copy of this message | ||
On Sun, Dec 6, 2020, at 7:00 PM, Paul Crovella wrote:
Hi Paul. Although we're on hold for a bit while Ilija makes some changes in direction (see
previous email), I wanted to respond to your concerns in particular because it sounds like
you're misunderstanding the scope and roadmap of what we're trying to do.
Enumerations, as a general concept, are stateless. Or rather, the idea of state doesn't even
apply to them. The integer 5 doesn't have any state of its own. All instances of the integer
5 are logically identical to all other instances of the integer 5.
Enumerations are a way to allow users to define that kind of "list of allowed values"
where each value is always identical to itself. Depending on the language they often have the
ability to define additional operations on that custom type, which are (from what I've found)
nearly always implemented in a method-like syntax for simplicity. In most languages that do so
it's straightforward because enums are built on top of objects for convenience, but for
instance C# doesn't use objects for enums but still has a way to attach method-like functions
to them. (See the survey review I did that's linked from the RFC.)
That the PHP RFC for enums uses objects internally is incidental. A few people suggested that we
use an entirely new zval type for it, which could no doubt be done and achieve the same end syntax
but would be a lot more work internally to do.
So if you're defining, say, an enum for cardinal directions (North, South, East, West), There
are literally only 4 possible values for that type, and those values must always be equal to
themselves. Attaching "5 miles" to "South" in one script and "2 km"
to "South" in another script is nonsensical, just like attaching "feet" to the
number 5 and later changing it to "fingers" just doesn't make any sense at all.
That's why, although we are building enums on top of objects, they cannot and must not have
mutable state. They can have predefined methods on them that return a static value (such as a
label() method), but that doesn't violate the "5 always === 5" rule. So anything
that offers a way to violate South === South cannot be allowed. If you want that, then you
don't want enums; you want classes. Don't get distracted by the word
"singleton" here. That's an implementation detail, not something the user is ever
exposed to.
It is true that, if you're attaching operations to enum cases that have no state, a method, a
static method, and a constant are often interchangeable. A constant is strictly less capable since
it cannot have logic, but a method and static method are functionally equivalent. The reason for
forbidding static methods and constants is largely simplicity. Instance methods can cover all of
those use cases, so it's only one mechanism to code and only mechanism for users to have to
think about. "Should I use mechanism X or Y when they are virtually identical?" is a
question that is best avoided. Too, it's a lot easier to add features later if we find they
would be useful than to take them away if we find they're problematic.
For example, the change I just discussed in my previous email about switching to a single class with
instances as constants means that static methods can only be on the enum (Suit), because there is no
per-case-class to put them on (Diamonds). The same is true of per-instance constants. If we'd
promised that those things worked, we wouldn't be able to make this change.
If when the dust settles there's a good reason to expand the functionality further in ways that
still don't violate "South === South," (such as adding constants to the enum type
itself, perhaps) those can be considered at that time.
As both Rowan and I mentioned, the long-term plan includes future RFCs for other functionality such
as tagged unions. If you want state, then what you want is not an enum but a tagged union. For
whatever reason, most languages with tagged unions seem to build them into and onto enums. I'm
not entirely sure why, and I'm honestly not 100% convinced that's the right approach in
PHP, but that's a debate for a future RFC. Within the scope of this RFC, enums must always
obey "South === South", and thus must be stateless. When we get to tagged unions we can
and likely will debate how state works there.
> Longer term plans are irrelevant except to avoid inadvertently
> shutting the door on something. This RFC is up for discussion, and
> will be up for voting, in isolation. It has to be able to stand on its
> own.
This is true up to a point. Yes, each RFC should be able to stand on its own, but that doesn't
mean it must be everything and the kitchen sink. Trying to add enums, tagged unions, pattern
matching, overridable identity, and generics all at once (since all of those *are* related to each
other) would result in a massive and unreviewable patch, a massive and unreviewable RFC, and people
not able to follow all the moving parts well enough to comment on them intelligently. That would
almost certainly fail.
Explicitly setting up a series of RFCs like we're doing is, as far as I am aware, novel for
PHP. It's the correct way to handle a larger task like this, however, especially when there
clear division points that "chunk" the work in natural ways. Even if enums in this RFC
are the only thing that passes, that's still a major win for PHP. It's not as big a win
as tagged unions with robust pattern matching and generics on the side would be, but it's still
a big step forward. By breaking it into chunks, we make it more likely that as much as can be done
will get done, and approved, and into the language, even if the end result is the same feature set
when 8.1.0 gets tagged.
--Larry Garfield