On Fri, Sep 5, 2025, at 22:01, Rowan Tommins [IMSoP] wrote: > On 4 September 2025 15:50:08 BST, Rob Landers <rob@bottled.codes> wrote: > >I think this is a fair observation and a fair question; but I think it is > >important not to have "magic". The power-of-two rule is to make it possible > >to work back how $enum->value === 15 (0x1111) even if you are completely new > >to the language. If you just use some magical cardinal order, it is > >impossible to reserve ranges, handle communications with external systems, > >etc. > > > A set does not need elements to have a defined order, only a defined > identity; that is available on any enum, even one with no backing at all. > That pure set could be serialised in various ways, based on the available > serialisations of its elements (comma-separated list, integer bitmask, binary > string bitmask, etc). That's the strongly typed model. > > The weakly typed model is to keep the values permanently in their serialised > form, and manipulate that directly. That is, you have a set of integers for > the flags, and construct a new integer for the set of flags. That has the > advantage of being simple and efficient, at the cost of safety and easy tool > affordance. > > What you're suggesting sounds like somewhere between the two: the individual > flags are of a specific type, rather than raw integers, but the set itself is > just an integer composed of their "backing values". The big downside I see is > that you can't natively label a parameter or return value as being a "set of > flags from this enum". You could probably make a docblock type annotation > work with an external static analyser, but in that case, you might as well > use that tool to enforce the powers of 2 on your enum or Plain Old Constants.
Interesting... but we don’t really have a way to say that a parameter or return value must satisfy *any* constraints except that it be of a certain type. Hmmm, I guess we have "true" and "false" as values that can be returned/accepted by functions — but I think they’re the only ones. A form of generics could solve *some* of this... an enum is a set, so what we really want to say is: "map this set of PHP identifiers to this other set of values". Which we already have via backed enums. What we lack is some set of operators to allow us to select subsets of the set and represent them. I was thinking of bitwise operators, because it seems the most natural. In fact, my extension approach was to just inject some operators on the class entry and call it a day. But with your response and my implementation that needed to deal with string-backed enums... The bitwise operators with enums would be a bit weird. Because | is a type union and saying Foo::PrettyPrint | Foo::ThrowOnError also kind of a union, but really, we’re constructing a new subset. So, maybe | isn’t the best operator. Then, when you would check, you use & (Foo::PrettyPrint & $val === 0)? It is not that elegant looking... idiomatic in languages like C, sure, but PHP ... not so much. If I were to step back, I’d probably end up with something like this: // use array-ish syntax to say that it is a subset of Foo $foo = Foo::[PrettyPrint, ThrowOnError]; // have a function accept any Foo but returns only specific subsets of Foo. function doFoo(Foo $anyFoo): Foo::[PrettyPrint, ThrowOnError]|Foo::[PrettyPrint]|null // check if a value of Foo intersects with another set of Foo return enum_intersects($anyFoo, Foo::[PrettyPrint]); In this case, backing values don’t matter at all. Serialization would still be an issue, but then again, it is already. > > Indeed, enforcing "this integer must be a power of 2" is not really anything > to do with enums, it would be useful on *any* type declaration. > > > Personally, I find the concept of enum cases having a single backing value > unnecessarily limiting, and would much prefer Java-style case properties. > > enum FooFlag: object { > case None(0, ''); > case ThrowOnError(0b1, 'T'); > // etc. > case PrettyPrint(0b1000, 'P'); > // etc. > > public function __construct(public int $flagForBinaryApi, public string > $flagForTextApi){} > } > > FooFlag::ThrowOnError->flagForBinaryApi; // 0b1 > FooFlag:: PrettyPrint->flagForTextApi; // 'P' > > > Ideally we would declare that $flagForBinaryApi must be a power of 2, and > that $flagForTextApi must be a single character, using some general-purpose > feature of the type system. > > The only thing that might be enum-specific is a way to say whether the values > of a property must be unique (something we don't currently have with > single-value backed enums). > > > Then the ideal would be an EnumSet customised to work with any properties or > methods it wanted: > > $foo = FooFlag::ThrowOnError | FooFlag::PrettyPrint; > $foo->serialiseForBinaryApi(); // 0b1001 > $foo->serialiseForTextApi(); // 'TP' > > > Rowan Tommins > [IMSoP] > — Rob