Hi everyone!
I'd like to open a discussion on this RFC, to auto-implement Stringable for
string-backed enums:
https://wiki.php.net/rfc/auto-implement_stringable_for_string_backed_enums
I'm looking forward to your feedback,
Cheers,
Nicolas
On Wed, Jun 22, 2022 at 12:47 AM Nicolas Grekas <
nicolas.grekas+php@gmail.com> wrote:
Hi everyone!
I'd like to open a discussion on this RFC, to auto-implement Stringable for
string-backed enums:
https://wiki.php.net/rfc/auto-implement_stringable_for_string_backed_enumsI'm looking forward to your feedback,
Hi Nicolas, Hi Ilija,
I would prefer if this was an explicit opt-in to have a __toString on a
backed enum. Maybe a special trait for enums that has the implementation,
so that a custom __toString cannot be implemented or a new syntax "enum Foo
: Stringable".
My concern is that auto implementing __toString, will lead to decreasing
type safety of enums in weak typing mode, since they get auto-casted to
string when passed to a function accepting strings. This effectively adds
more type juggling cases.
The example in the RFC about attributes accepting strings or Enums can be
solved by union types on the side of the library developers, it doesn't
need to be magically implemented by the engine.
I don't consider "use strict mode" a good argument to avoid this problem,
because that has other downsides such as overcasting.
Cheers,
Nicolas
Hi everyone!
I'd like to open a discussion on this RFC, to auto-implement Stringable for
string-backed enums:
https://wiki.php.net/rfc/auto-implement_stringable_for_string_backed_enumsI'm looking forward to your feedback,
I am not in favour of this RFC, especially because it is not opt-in.
- Enums were introduced to add extra type safety. Allowing them to degrade to simple strings reduces that type safety.
- It is not implemented for all enum types, only for string backed ones. This change would now make different classes of enums, without them being differently inherited classes.
- The main use case seems to be to prevent having to type ->value, which would otherwise indicate reliably whether an enum is used as string argument.
I won't be voting in favour of this RFC as it currently stands.
cheers
Derick
Hi Benjamin and Derick,
I'm replying to both of you because I see some things in common in your
comments.
https://wiki.php.net/rfc/auto-implement_stringable_for_string_backed_enums
I would prefer if this was an explicit opt-in to have a __toString on a
backed enum. Maybe a special trait for enums that has the implementation,
so that a custom __toString cannot be implemented or a new syntax "enum Foo
: Stringable".
We can make this opt-in by simply allowing user-land to implement
Stringable.
This is a different solution to the problem the RFC tries to solve and I
would also personally be fine with.
I would then not try to limit which implementation of it is allowed by
the engine. Instead, I would give all powers to user-land to decide on
their own what makes sense for their use case. As a general principle, I
believe that empowering user-land is always a win vs trying to "save them
from themselves", as if we knew better than them what they want to achieve,
and especially how.
My concern is that auto implementing __toString, will lead to decreasing
type safety of enums in weak typing mode, since they get auto-casted to
string when passed to a function accepting strings. This effectively adds
more type juggling cases.
I don't share this concern: if an API accepts a string and the engine can
provide a string, let it do so. There is nothing inherently dangerous in
doing so. But we don't need to agree on that if the proposal above makes
sense to everybody :)
The example in the RFC about attributes accepting strings or Enums can be
solved by union types on the side of the library developers, it doesn't
need to be magically implemented by the engine.
I extensively explain in the RFC why this should not be on the side of lib
authors, but on the side of end-users. Please double check and let me know
your thoughts.
I don't consider "use strict mode" a good argument to avoid this problem,
because that has other downsides such as overcasting.
I'm 100% aligned with that.
- It is not implemented for all enum types, only for string backed ones.
This change would now make different classes of enums, without them being
differently inherited classes.
This would also be solved by allowing user-land to implement Stringable on
all kind of enums. Would that make sense to you?
- The main use case seems to be to prevent having to type ->value, which
would otherwise indicate reliably whether an enum is used as string
argument.
Yep, that's a "strict mode" approach and this RFC mostly applies to
non-strict mode.
There are also cases where using "->value" is just not possible. I mention
attributes in the RFC, but we also have a case in Symfony where defining
service definitions in yaml doesn't work with enums because there is no way
to express the "->value" part.
If that's the consensus, I'm fine updating the RFC to turn the vote into
whether "allowing user-land to implement Stringable on any kind of enums"
is desired or not.
Nicolas
On Wed, Jun 22, 2022 at 6:01 PM Nicolas Grekas nicolas.grekas+php@gmail.com
wrote:
Hi Benjamin and Derick,
I'm replying to both of you because I see some things in common in your
comments.https://wiki.php.net/rfc/auto-implement_stringable_for_string_backed_enums
I would prefer if this was an explicit opt-in to have a __toString on a
backed enum. Maybe a special trait for enums that has the implementation,
so that a custom __toString cannot be implemented or a new syntax "enum Foo
: Stringable".We can make this opt-in by simply allowing user-land to implement
Stringable.
This is a different solution to the problem the RFC tries to solve and I
would also personally be fine with.
I would then not try to limit which implementation of it is allowed by
the engine. Instead, I would give all powers to user-land to decide on
their own what makes sense for their use case. As a general principle, I
believe that empowering user-land is always a win vs trying to "save them
from themselves", as if we knew better than them what they want to achieve,
and especially how.My concern is that auto implementing __toString, will lead to decreasing
type safety of enums in weak typing mode, since they get auto-casted to
string when passed to a function accepting strings. This effectively adds
more type juggling cases.I don't share this concern: if an API accepts a string and the engine can
provide a string, let it do so. There is nothing inherently dangerous in
doing so. But we don't need to agree on that if the proposal above makes
sense to everybody :)The example in the RFC about attributes accepting strings or Enums can be
solved by union types on the side of the library developers, it doesn't
need to be magically implemented by the engine.I extensively explain in the RFC why this should not be on the side of lib
authors, but on the side of end-users. Please double check and let me know
your thoughts.
I don't fully agree with your argument in the RFC.
You mention as an exmaple that users want to pass PossibleRoles enum values
into the IsGranted attribute. But the way Symfony works as you know, you
can pass arbitrary role names here from the library POV, and applications
can add their own. So naturally the API is to pass a string. So
"PossibleRoles" can only be a user provided enum, it can never be a Symfony
type.
I don't see how PHP should provide a workaround for Symfony and its users
wanting to allow users to use an arbitrary enum as input to a function.
In this example, in Symfony code you only expect "any" BackedEnum and
cannot validate that it is a value of PossibleRoles enum. As such Rowan
suggestion to use constants is the way Symfony should recommend their users.
I don't consider "use strict mode" a good argument to avoid this problem,
because that has other downsides such as overcasting.I'm 100% aligned with that.
- It is not implemented for all enum types, only for string backed ones.
This change would now make different classes of enums, without them being
differently inherited classes.This would also be solved by allowing user-land to implement Stringable on
all kind of enums. Would that make sense to you?
- The main use case seems to be to prevent having to type ->value, which
would otherwise indicate reliably whether an enum is used as string
argument.Yep, that's a "strict mode" approach and this RFC mostly applies to
non-strict mode.
There are also cases where using "->value" is just not possible. I mention
attributes in the RFC, but we also have a case in Symfony where defining
service definitions in yaml doesn't work with enums because there is no way
to express the "->value" part.If that's the consensus, I'm fine updating the RFC to turn the vote into
whether "allowing user-land to implement Stringable on any kind of enums"
is desired or not.Nicolas
Hi Nicolas, thanks for the RFC,
There are also cases where using "->value" is just not possible. I mention
attributes in the RFC,
which also mentions
https://wiki.php.net/rfc/fetch_property_in_const_expressions (but with
"For people that use non-strict mode, this extra “->value” is
boilerplate that they'd better remove")
but we also have a case in Symfony where defining
service definitions in yaml doesn't work with enums because there is no way
to express the "->value" part.
Symfony YAML has a !php/const X
feature, which also works when X is
an Enum::CASE; how about a !php/enum_value
feature?
Otherwise, I also like Rowan's suggestion of implementing "internal
cast handlers", so that non-strict users could call e.g.
takes_int(IntEnum::CASE)
as well as
takes_string(StringEnum::CASE)
; but what about
takes_string(IntEnum::CASE)
, and
takes_Stringable(StringEnum::CASE)
?
In any case, several people requested that it should require to be
opted-in explicitly; but then [for solutions other than "allowing
user-land to implement Stringable"] we probably also need a way to
test whether a BackedEnum [instance] is "coercible"?
Regards,
--
Guilliam Xavier
Hi Guilliam,
There are also cases where using "->value" is just not possible. I
mention
attributes in the RFC,which also mentions
https://wiki.php.net/rfc/fetch_property_in_const_expressions (but with
"For people that use non-strict mode, this extra “->value” is
boilerplate that they'd better remove")
Absolutely.
Providing a nice and expressive syntax is critical. We added e.g. short
closures, constructor property promotion, etc. all because we care to not
write ugly code.
With the "Fetch properties of enums in const expressions" RFC, we soon
might be able to reference values in enums with this syntax:
#[MyAttr(MyEnum::Case->value)]
I understand why we're considering this and this might be only me, but I
find this ugly.
The same happens when coding: take_string($enum->value)
This ugliness is part of the problem I'd like to solve. Rowan's proposals
about sets could solve this in a very nice way , but we're not there yet. I
can wait though.
Symfony YAML has a
!php/const X
feature, which also works when X is
an Enum::CASE; how about a!php/enum_value
feature?
I submitted something similar today at
https://github.com/symfony/symfony/pull/46771
Otherwise, I also like Rowan's suggestion of implementing "internal
cast handlers", so that non-strict users could call e.g.
I had a look at gmp for example: cast handlers don't work when calling a
function.
They do work when explicit casting and when doing loose comparisons, but
they don't when calling functions (or returning from one.) I don't know the
underlying reason for that behavior but it looks consistent. Unfortunately
that idea looks like a dead end.
In any case, several people requested that it should require to be
opted-in explicitly; but then [for solutions other than "allowing
user-land to implement Stringable"] we probably also need a way to
test whether a BackedEnum [instance] is "coercible"?
I don't have a solution that meets all the requirements that ppl expressed
so far.
If I could get back in time, I would enable that missing casting rule I
described for gmp in non-strict mode, and I would implement it on backed
enums from the start.
I'm now considering withdrawing the RFC because I don't see a way forward
that could be consensual enough.
If other ppl share my concerns and have a proposal, please let us know.
Nicolas
Hi Nicolas,
On Fri, 24 Jun 2022 at 17:38, Nicolas Grekas
nicolas.grekas+php@gmail.com wrote:
I'm now considering withdrawing the RFC because I don't see a way forward
that could be consensual enough.
Just in general, I think changes to the type system are always going
to take longer than a few weeks to discuss. There are always going to
be subtle implications that need to be thought through thorougly.
Also, the argument for a change to the type system like this should be
based on why it's the right thing to do for new code that is being
written. Although obviously everyone who has a large code base wants
to be able to upgrade to the latest and greatest PHP to get the new
features at the lowest cost in changing code, imo it's more important
long term to get the language right, rather than putting too much
emphasis on lowering the cost of upgrading.
Derick wrote:
My concern is that auto implementing __toString, will lead to decreasing
type safety of enums in weak typing mode, since they get auto-casted to
string when passed to a function accepting strings. This effectively adds
more type juggling cases.I don't share this concern: if an API accepts a string and the engine can
provide a string,
function/method calls are not the only place were type comparisons are done.
My understanding is that if this RFC is passed, then the situation would be:
Foo::Bar == 'Bar'; // true
Baz::Bar == 'Bar'; // true
Baz::Bar == Foo::Bar; // false
Which is not obviously the correct thing.
There is also the issue that callbacks called internally by the engine
are always in weak/coercive mode. That means that changes to
weak/coercive can leak through to code that wants to always be in
strict mode.
It would be great if we could find ways to make it easier for general-purpose
libraries to support enums without cluttering libraries with if/else blocks
and without changing existing interfaces in a backwards-incompatible way.
There's two parts going on here. First, yes, enums could probably be
easier to work with, and we should probably be looking how to do that.
Second is an argument about how easy it should be to retrofit new
features to existing code. Although I can see why people would want
that, it's a lot harder to justify it.
As experienced on the Symfony repository, this problem is especially visible
at the boundary of libraries: when some component accepts a string as input,
ppl want them to also accept backed-enums. This usually means that they
propose widening the accepted types of some method to make them work
seamslessly with enums.
Doing that appears to be a violation of the "open for extension,
closed for modification" principle.
If you want to change the type signature, introducing a new function
is almost certainly the 'right' thing to do, even if that means more
work for people with large existing code-bases. e.g.
Change:
class Foo {
function bar(string $quux) {}
}
To this:
class Foo {
function bar(string $quux) {}
function barEx(string|SomeEnum $quux) {}
}
And eventually deprecate the original bar method.
but we also have a case in Symfony where defining service
definitions in yaml doesn't work with enums because there
is no way to express the "->value" part.
I very strongly think the limitations of what yaml config file choices
were made in a downstream project should not influence the design of a
the type system of an upstream project. I'm pretty sure that a
successful RFC isn't going to need to mention that problem.
If other ppl share my concerns and have a proposal, please let us know.
It might be hard to have a one size fits all rule, hard-coded in the
engine as people genuinely have different views of what enums are.
One idea that I think may have been mentioned before (and if I recall,
shot down pretty hard) would be to allow people to register cast
callbacks similar to the code below. That would allow people who view
enums as special constants to cast away, and those view enums as
types, to not cast.
This currently would be problematic for the same reason that PHP ini
settings are problematic; without a module or package system, any
settings that affect how the engine behave affect all code that is
run, rather than being limited to just the library that wants to
enable that setting. Which is a problem that keeps rearing it's head.
Maybe someone sponsoring some blue-sky research on how feasible a
module/package system could be, would make addressing problems similar
to the one here be easier to work on.
cheers
Dan
Ack
enum Suit: string {
case Hearts = 'H';
case Spades = 'S';
}
function foo(string $bar) {
var_dump($bar);
}
function cast_backed_enum_to_string(BackedEnum $enum): string {
return $enum->value;
}
register_cast_callback(
BackedEnum::class, // source type
'string', // target type
cast_backed_enum_to_string(...)
);
foo(Suit::Hearts);
// Engine sees we have a BackedEnum and that foo wants a
// string, so calls cast_backed_enum_to_string to do the conversion
Symfony YAML has a
!php/const X
feature, which also works when X is
an Enum::CASE; how about a!php/enum_value
feature?I submitted something similar today at https://github.com/symfony/symfony/pull/46771
And I see that it has been merged ;)
Otherwise, I also like Rowan's suggestion of implementing "internal
cast handlers", so that non-strict users could call e.g.I had a look at gmp for example: cast handlers don't work when calling a function.
They do work when explicit casting and when doing loose comparisons, but they don't when calling functions
Well it works for string
at least (even though GMP
does not
implement __toString
), e.g. https://3v4l.org/cRnnW
The TypeError for int
may be related to the fact that e.g. +$gmp
(or 0 + $gmp
) gives back a GMP (not an int like (int)$gmp
)?
(But indeed I don't know much about that... nor if it could be made opt-in)
Rowan's proposals about sets could solve this in a very nice way
Reminded me of e.g.
https://stackoverflow.com/questions/6422380/does-any-programming-language-support-defining-constraints-on-primitive-data-types
(mainly integer ranges, but the concept of "domains" looks similar)
Regards,
--
Guilliam Xavier
Reminded me of e.g.
https://stackoverflow.com/questions/6422380/does-any-programming-language-support-defining-constraints-on-primitive-data-types
(mainly integer ranges, but the concept of "domains" looks similar)
Yes, I'm vaguely familiar with the facilities provided by Pascal (via
Delphi) and SQL (via Postgres).
Looking at the Postgres docs, it has both "enumerated" and "domain"
types, with exactly the distinction that's relevant here.
Enums are type safe:
Each enumerated data type is separate and cannot be compared with
other enumerated types
https://www.postgresql.org/docs/current/datatype-enum.html
They can be explicitly cast to "text" (i.e. string), but will not be
cast implicitly when passed to a function or operator, so it ends up
equivalent to our case of calling ->value, but without the flexibility
of distinguishing the "case name" and "backing value".
Whereas domains are sub-types of some other type:
When an operator or function of the underlying type is applied to a
domain value, the domain is automatically down-cast to the underlying type.
https://www.postgresql.org/docs/current/domains.html
This seems to be what people are asking for here: the values are
constrained when the domain is explicitly mentioned, but freely
interchangeable with the underlying type.
Here's an online demo showing the difference:
https://dbfiddle.uk/?rdbms=postgres_14&fiddle=88639144aec58ab7cf7e34a0c103aa51
Regards,
--
Rowan Tommins
[IMSoP]
On 21 June 2022 23:47:15 BST, Nicolas Grekas
nicolas.grekas+php@gmail.com wrote:Hi everyone!
I'd like to open a discussion on this RFC, to auto-implement Stringable for
string-backed enums:
https://wiki.php.net/rfc/auto-implement_stringable_for_string_backed_enumsI'm looking forward to your feedback,
I am not in favour of this RFC, especially because it is not opt-in.
- Enums were introduced to add extra type safety. Allowing them to
degrade to simple strings reduces that type safety.- It is not implemented for all enum types, only for string backed
ones. This change would now make different classes of enums, without
them being differently inherited classes.- The main use case seems to be to prevent having to type ->value,
which would otherwise indicate reliably whether an enum is used as
string argument.I won't be voting in favour of this RFC as it currently stands.
cheers
Derick
I am also still opposed to this proposal, and the more I think on it the more opposed I get.
Here's the basic problem. There are two use cases (broadly speaking) where this would be applicable. In one, a parameter is string typed today but there is a finite number of legal values, logically. Think $order = ASC/DESC. In the other, the parameter is string typed today and there is a technically infinite number of legal values, from the point of view of that function. In a given use case there may be a finite set we want to use, but from the function's point of view, it's nominally infinite. That's the "roles" example that Nicolas cites.
In case 1, I'd argue that the function should be switching to an Enum long term and dropping the string. For that, a union type is the optimal solution. Does that have BC implications for sub-classes? Well, yes, but so does any type improvement. This is a known problem space, with known solutions and migration strategies. It's conceptually no different from migrating from "this takes a string" to "this takes an array of strings": Widen the type, have transitional code, retighten the type. The time frame for that could be weeks, months, or years depending on the situation, but it's not a novel concept.
In case two, an enum is simply the wrong tool. Every time I say this someone whines that I'm being elitist or judgemental or holier than thou or whatever, but... they're different types. If someone proposed "I want to be able to pass a single-element array to a string parameter and have it automatically unwrap the array", it wouldn't be taken seriously. That's what's being proposed here, though.
If an access control system works on roles as strings, then... it expects a string. It doesn't expect "one of these and only these values, defined at code time." The application may be written for that, but the access check function does not. It works on strings. Passing it a non-string should be an error, just as much as passing it an array should be.
The argument presented is that it's easier to type AppRoles::Admin
than "admin"
, because the former provides you with an error if you typo something. That's a valid argument, but... not for using enums. It's an argument for using constants.
class AppRoles
{
public const Admin = 'admin';
public const Editor = 'editor';
public const User = 'user';
}
enum AppRoles: string
{
case Admin = 'admin';
case Editor = 'editor';
case User = 'user';
}
It's basically the same work to setup, but one does exactly what it says: It provides syntax-checked shortcuts for strings, which is what the API wants. Using an enum here instead of constants provides exactly zero additional value, because the limited-set-ness of the enum won't be checked in the first place.
Just because someone wants to use the claw-side of a hammer as a screwdriver doesn't mean we should design it to be a screwdriver. They should just use a screwdriver.
And for those arguing that we shouldn't be "protecting users from themselves"... that's exactly what types are. That's exactly what they're for. The whole reason to have typed parameters is to stop people from doing things that make no sense. That's the point.
So I am firmly against making it easier to (mis)use enums in a situation where constants are already the superior solution by every metric. The only argument I see is making case 1, transitioning from a string to an enum for a genuinely limited-case, easier. But in that case, the transition is going to have to happen eventually anyway, and that means the type is going to change at some point, and the same BC issue will appear, just at a different time. Unless the intent is to then never change the type and keep the function incorrectly typed (from the POV that it's logically an enum, even though string typed was the best/correct type for years) forever, in which case... use a set of constants.
--Larry Garfield
Hi
In case 1, I'd argue that the function should be switching to an Enum long term and dropping the string. For that, a union type is the optimal solution. Does that have BC implications for sub-classes? Well, yes, but so does any type improvement. This is a known problem space, with known solutions and migration strategies. It's conceptually no different from migrating from "this takes a string" to "this takes an array of strings": Widen the type, have transitional code, retighten the type. The time frame for that could be weeks, months, or years depending on the situation, but it's not a novel concept.
Exactly this. It might be painful for existing code, but I expect any
newly written and any updated code to handle enums "natively" and so
this is something that will solve itself over time.
On the other hand once the enum type safety is watered up by allowing
implicit string conversions, there is no easy way to revert this.
Best regards
Tim Düsterhu
On Wed, Jun 22, 2022 at 8:27 PM Larry Garfield larry@garfieldtech.com
wrote:
So I am firmly against making it easier to (mis)use enums in a situation
where constants are already the superior solution by every metric. The
only argument I see is making case 1, transitioning from a string to an
enum for a genuinely limited-case, easier. But in that case, the
transition is going to have to happen eventually anyway, and that means the
type is going to change at some point, and the same BC issue will appear,
just at a different time. Unless the intent is to then never change the
type and keep the function incorrectly typed (from the POV that it's
logically an enum, even though string typed was the best/correct type for
years) forever, in which case... use a set of constants.
Hi!
I'm with you on what you mentioned here.
But also, I think the need I understood arises from another case that is
neither 1 or 2.
When you have two domains the value might need to be represented as a
backed enum in one side and as a string in the other.
As far as I understood, this is the case, with applications that are in one
domain wants to have a proper enum for let's say the app roles as the
possible roles are just a limited set.
That application is using another library to configure the ACL using those
roles and this is another domain that does not have a limited value on the
role representation, it's just a string.
Naturally, you should just transform the enum instance to the string and
that should be done using the value property. But this does not work for
configurations done through attribute parameters.
And I think this is the only problem we should fix and that's fixable by
https://wiki.php.net/rfc/fetch_property_in_const_expressions
What you mentioned about developers using enum in the wrong way is
completely true and it's been a long effort for me in explaining this.
I was hoping it would be diminished somehow by increased popularity of
enums, now that they are supported by everyone. But the usages are also
increasing.
Regards,
Alex
On Wed, Jun 22, 2022 at 8:27 PM Larry Garfield larry@garfieldtech.com
wrote:So I am firmly against making it easier to (mis)use enums in a situation
where constants are already the superior solution by every metric. The
only argument I see is making case 1, transitioning from a string to an
enum for a genuinely limited-case, easier. But in that case, the
transition is going to have to happen eventually anyway, and that means the
type is going to change at some point, and the same BC issue will appear,
just at a different time. Unless the intent is to then never change the
type and keep the function incorrectly typed (from the POV that it's
logically an enum, even though string typed was the best/correct type for
years) forever, in which case... use a set of constants.Hi!
I'm with you on what you mentioned here.But also, I think the need I understood arises from another case that is
neither 1 or 2.
When you have two domains the value might need to be represented as a
backed enum in one side and as a string in the other.
As far as I understood, this is the case, with applications that are in one
domain wants to have a proper enum for let's say the app roles as the
possible roles are just a limited set.
That application is using another library to configure the ACL using those
roles and this is another domain that does not have a limited value on the
role representation, it's just a string.
Naturally, you should just transform the enum instance to the string and
that should be done using the value property. But this does not work for
configurations done through attribute parameters.
And I think this is the only problem we should fix and that's fixable by
https://wiki.php.net/rfc/fetch_property_in_const_expressions
Yes, I'm planing to vote +1 on that RFC. Although in that case, the same logic for why to use a constant instead still applies.
What you mentioned about developers using enum in the wrong way is
completely true and it's been a long effort for me in explaining this.
I was hoping it would be diminished somehow by increased popularity of
enums, now that they are supported by everyone. But the usages are also
increasing.Regards,
Alex
I may need to publish a blog post on this issue specifically that Symfony and others can point to when people keep asking them to do the wrong thing. I fully understand that major frameworks and libraries (Symfony et al) are getting the pushback of people trying to do the wrong thing, but the solution should be better educational efforts so people stop trying to do the wrong thing, not making it easier to do the wrong thing.
--Larry Garfield
The argument presented is that it's easier to type
AppRoles::Admin
than"admin"
, because the former provides you with an error if you typo something. That's a valid argument, but... not for using enums. It's an argument for using constants.
I wonder if the reality is that neither enums (as implemented) nor
constants are the right solution to to this.
What users want is some way to say "this value should be a string, but
in this context it should be one of this list of strings"; or,
sometimes, "is this string one of this list of strings?" Constants can't
do that - they give you a way of referring to the possible values, but
no tools for enforcing or testing against them. Backed enums can kinda
sorta do that with a bit of effort, using ->value and ::tryFrom, but
they're not really built for it.
A better fit would be some kind of "domain type", which would allow you
to write something vaguely like this:
domain SymfonyPermission: string;
domain AcmePermission: string { 'admin' | 'user' | 'bot' };
assert( in_domain('admin', SymfonyPermission) );
assert( in_domain('admin', AcmePermission) );
assert( in_domain('random stranger', SymfonyPermission) );
assert( ! in_domain('random stranger', AcmePermission) );
Domains can also be considered sets, which you could compare directly,
and maybe even calculate intersections, unions, etc:
assert( is_subset(AcmePermission, SymfonyPermission) );
The actual values would be ordinary strings, and type constraints would
just be checking the value passed against the domain:
function doSymfonyThing(SymfonyPermission $permission) {
echo $permission; // no coercion needed, $permission is a string
}
function doAcmeThing(AcmePermission $permission) {
doSymfonyThing($permission);
}
doAcmeThing('admin'); // no special syntax needed to "construct" or
"look up" an instance
Crucially, this solves the described problem of a library accepting an
infinite (or perhaps just very wide) set of values, and a consuming app
wanting to constrain that set within its own code.
It's one disadvantage is the typo-proofing and look up availability that
constants give, but you could always combine the two.
Regards,
--
Rowan Tommins
[IMSoP]
The argument presented is that it's easier to type
AppRoles::Admin
than"admin"
, because the former provides you with an error if you typo something. That's a valid argument, but... not for using enums. It's an argument for using constants.I wonder if the reality is that neither enums (as implemented) nor
constants are the right solution to to this.What users want is some way to say "this value should be a string, but
in this context it should be one of this list of strings"; or,
sometimes, "is this string one of this list of strings?" Constants can't
do that - they give you a way of referring to the possible values, but
no tools for enforcing or testing against them. Backed enums can kinda
sorta do that with a bit of effort, using ->value and ::tryFrom, but
they're not really built for it.A better fit would be some kind of "domain type", which would allow you
to write something vaguely like this:domain SymfonyPermission: string;
domain AcmePermission: string { 'admin' | 'user' | 'bot' };assert( in_domain('admin', SymfonyPermission) );
assert( in_domain('admin', AcmePermission) );assert( in_domain('random stranger', SymfonyPermission) );
assert( ! in_domain('random stranger', AcmePermission) );Domains can also be considered sets, which you could compare directly,
and maybe even calculate intersections, unions, etc:assert( is_subset(AcmePermission, SymfonyPermission) );
The actual values would be ordinary strings, and type constraints would
just be checking the value passed against the domain:function doSymfonyThing(SymfonyPermission $permission) {
echo $permission; // no coercion needed, $permission is a string
}function doAcmeThing(AcmePermission $permission) {
doSymfonyThing($permission);
}doAcmeThing('admin'); // no special syntax needed to "construct" or
"look up" an instanceCrucially, this solves the described problem of a library accepting an
infinite (or perhaps just very wide) set of values, and a consuming app
wanting to constrain that set within its own code.It's one disadvantage is the typo-proofing and look up availability that
constants give, but you could always combine the two.
Interesting concept. I'm not sure if I like it yet, but it's interesting. :-) It somehow feels related to Go's type aliasing, but I'm not sure if that's a fair comparison.
Is there a type-theoretic basis we could look at for that? It seems like the sort of thing some mathematician has likely thought through for funsies before.
--Larry Garfield
domain SymfonyPermission: string;
domain AcmePermission: string { 'admin' | 'user' | 'bot' };
[...]
Domains can also be considered sets, which you could compare directly,
and maybe even calculate intersections, unions, etc:The actual values would be ordinary strings, and type constraints would
just be checking the value passed against the domain:Crucially, this solves the described problem of a library accepting an
infinite (or perhaps just very wide) set of values, and a consuming app
wanting to constrain that set within its own code.It's one disadvantage is the typo-proofing and look up availability that
constants give, but you could always combine the two.
Thanks for this idea Rowan, that's really interesting.
I would go one step further and require naming the values in the set.
Borrowing from the syntax of enums, we could have:
set AcmePermission: string {
case ADMIN = 'admin';
case USER = 'user';
case BOT = 'bot';
}
Then AcmePermission::ADMIN would === 'admin' and, as you said,
AcmePermission could be used as a type on functions:
function (AcmePermission $perm): AcmePermission
{
$perm instanceof AcmePermission; // true
return $perm;
}
In order to make this work, we would need to be able to autoload the type
AcmePermission.This could be done by the type-checking logic: when checking
$var against AcmePermission and the type is undefined, if $var is an int or
a string, call the autoloader for 'AcmePermission'.
There are other things to consider, like possible changes to reflection,
whether such a set also declares a class like enums do, etc.But overall, I
really like it and I agree with you: this would provide a really good
solution for the problem ppl try to use enums for.
I don't know how we could move forward on that idea. By starting another
thread?
Nicolas
So I am firmly against making it easier to (mis)use enums in a
situation where constants are already the superior solution by every
metric. The only argument I see is making case 1, transitioning from a
string to an enum for a genuinely limited-case, easier. But in that
case, the transition is going to have to happen eventually anyway, and
that means the type is going to change at some point, and the same BC
issue will appear, just at a different time. Unless the intent is to
then never change the type and keep the function incorrectly typed
(from the POV that it's logically an enum, even though string typed was
the best/correct type for years) forever, in which case... use a set of
constants.--Larry Garfield
As promised, a blog post on the topic that folks are welcome to link to when telling people no:
https://peakd.com/hive-168588/@crell/on-the-use-of-enums
--Larry Garfield
On Tue, 21 Jun 2022 at 23:47, Nicolas Grekas nicolas.grekas+php@gmail.com
wrote:
Hi everyone!
I'd like to open a discussion on this RFC, to auto-implement Stringable for
string-backed enums:
https://wiki.php.net/rfc/auto-implement_stringable_for_string_backed_enums
Hi Nicolas,
Like others, I'm lukewarm on this - but then I've always been lukewarm on
both Backed Enums and __toString
For me, the big value of an enum is as a value that is never equal to
anything but itself, so the idea of MyEnum::Foo == 'foo' evaluating to true
feels instinctively wrong to me.
Meanwhile on the string side, I feel like most objects have more than one
string representation, so "blessing" one makes little sense. Taking the
example of permissions given in the PR discussion, what if you need to pass
these same permissions to two different libraries, which have different
format requirements?
However, we DO have backed enums, and people clearly want to use them for
this scenario, so I'm tentatively OK with allowing them to do so, since I
can just use a non-backed enum to get my preferred "style".
Derick raises a good point about it not working for int-backed enums,
though. What if, rather than implementing __toString() as such, we
implemented internal cast handlers for either string or int, depending on
the backing value? (Other internal objects do have this ability, e.g. GMP
and SimpleXMLElement)
Regards,
Rowan Tommins
[IMSoP]