[RFC] [Under Discussion] Auto-implement Stringable for string backed enums

  118040
June 21, 2022 22:47 nicolas.grekas+php@gmail.com (Nicolas Grekas)
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
  118045
June 22, 2022 12:33 kontakt@beberlei.de (Benjamin Eberlei)
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_enums > > I'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 >
  118046
June 22, 2022 13:05 derick@php.net (Derick Rethans)
On 21 June 2022 23:47:15 BST, Nicolas Grekas 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 > >I'm looking forward to your feedback,
I am not in favour of this RFC, especially because it is not opt-in. 1. Enums were introduced to add extra type safety. Allowing them to degrade to simple strings reduces that type safety. 2. 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. 3. 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
  118054
June 22, 2022 16:01 nicolas.grekas+php@gmail.com (Nicolas Grekas)
Hi Benjamin and Derick,

I'm replying to both of you because I see some things in common in your
comments.



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. 2. 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?
> 3. 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
  118068
June 23, 2022 08:07 kontakt@beberlei.de (Benjamin Eberlei)
On Wed, Jun 22, 2022 at 6:01 PM Nicolas Grekas 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. > > 2. 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? > > >> 3. 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 >
  118076
June 23, 2022 15:50 guilliam.xavier@gmail.com (Guilliam Xavier)
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
  118091
June 24, 2022 16:37 nicolas.grekas+php@gmail.com (Nicolas Grekas)
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
  118095
June 25, 2022 14:44 Danack@basereality.com (Dan Ackroyd)
Hi Nicolas,

On Fri, 24 Jun 2022 at 17:38, Nicolas Grekas
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
  118104
June 27, 2022 12:03 guilliam.xavier@gmail.com (Guilliam Xavier)
>> 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
  118110
June 27, 2022 18:01 rowan.collins@gmail.com (Rowan Tommins)
On 27/06/2022 13:03, Guilliam Xavier wrote:
> 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]
  118058
June 22, 2022 17:26 larry@garfieldtech.com ("Larry Garfield")
On Wed, Jun 22, 2022, at 8:05 AM, Derick Rethans wrote:
> On 21 June 2022 23:47:15 BST, Nicolas Grekas > 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 >> >>I'm looking forward to your feedback, > > I am not in favour of this RFC, especially because it is not opt-in. > > 1. Enums were introduced to add extra type safety. Allowing them to > degrade to simple strings reduces that type safety. > 2. 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. > 3. 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
  118060
June 22, 2022 17:52 tim@bastelstu.be (=?UTF-8?Q?Tim_D=c3=bcsterhus?=)
Hi

On 6/22/22 19:26, Larry Garfield wrote:
> 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
  118063
June 22, 2022 19:31 drealecs@gmail.com (=?UTF-8?Q?Alexandru_P=C4=83tr=C4=83nescu?=)
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
  118074
June 23, 2022 15:32 larry@garfieldtech.com ("Larry Garfield")
On Wed, Jun 22, 2022, at 2:31 PM, Alexandru Pătrănescu wrote:
> 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
  118066
June 22, 2022 21:43 rowan.collins@gmail.com (Rowan Tommins)
On 22/06/2022 18:26, Larry Garfield wrote:
> 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]
  118075
June 23, 2022 15:35 larry@garfieldtech.com ("Larry Garfield")
On Wed, Jun 22, 2022, at 4:43 PM, Rowan Tommins wrote:
> On 22/06/2022 18:26, Larry Garfield wrote: >> 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.
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
  118089
June 24, 2022 16:11 nicolas.grekas+php@gmail.com (Nicolas Grekas)
> 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
  118109
June 27, 2022 16:54 larry@garfieldtech.com ("Larry Garfield")
On Wed, Jun 22, 2022, at 12:26 PM, Larry Garfield 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. > > --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
  118057
June 22, 2022 17:22 rowan.collins@gmail.com (Rowan Tommins)
On Tue, 21 Jun 2022 at 23:47, Nicolas Grekas 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]