On Sat, Nov 16, 2024, at 5:14 PM, Rob Landers wrote: > Hello internals, > > I'm ready as I'm going to be to introduce to you: "Records" > https://wiki.php.net/rfc/records! > > Records allow for a lightweight syntax for defining value objects. > These are superior to read-only classes due to having value semantics > and far less boilerplate, for most things developers use read-only > classes for. They are almost as simple to use as arrays (and provide > much of the same semantics), but typed. > > As an example, if you wanted to define a simple User record: > > record User(string $emailAddress, int $databaseId); > > Then using it is as simple as calling it like a function, with the & symbol: > > $rob = &User("rob@bottled.codes", 1); > > Since it has value semantics, we can get another instance, and it is > strongly equal to another of the same parameters: > > $otherRob = &User("rob@bottled.codes", 1); > assert($otherRob === $rob); // true > > Records may also have methods (even hooks), use traits, and implement > interfaces: > > record Vector3(float $x, float $y, $z) implements Vector { > use Vector; > public float magnitude { > get => return sqrt($this->x ** 2 + $this->y ** 2 + $this->z ** 2) > } > } > > Further, an automatic (but overridable) "with" method is generated for > every record. This allows you to get a new record similar to a given > one, very easily: > > record Planet(string $name); > > $earth = &Planet("earth"); > $mars = $earth->with(name: "mars"); > > The depth of records was an immense exploration of the PHP engine, > language design, and is hopefully quite powerful for the needs of > everyday PHP and niche libraries. I took care in every aspect and tried > to cover every possible case in the RFC, but I still probably missed > some things. I plan on having a full implementation done by the end of > the year and open to a vote by the end of January, but I'd like to open > the discussion up here first. Love it or hate it, I'd like to hear your > thoughts. > > — Rob
Hi Rob. I appreciate the amount of work that's gone into this, and I share most of its spiritual goals. However, as I have discussed in various threads before, I believe a separate value type is the wrong approach to take. One of my guiding principles is that features should be focused, targeted, and designed to integrate well with other features. (That doesn't always mean small, just focused.) It should be easy for devs to cleanly cherry pick what features they want to use, and not be forced into all-or-nothing situations where it can be avoided. That's why I do not believe bundling a bunch of object-ish features into a new construct that is almost but not quite an object is wise. Every one of those features I can see wanting to use stand-alone on a regular object. I see these features collected here: * Immutable object * Inline constructors * value-style passing * dedicated evolvable syntax (the with() method) * alternate creation syntax (&RecordName) * value-based strong-equality That's a half-dozen features that I can see a good argument for wanting on objects, without all the others. As Ilija notes, immutable objects are not always the answer. I like them, and use them frequently, but they're not always appropriate. And we already have them with either readonly classes or now private(set) (which is close enough to immutable 99.4% of the time) I can see the benefit of an inline constructor. Kotlin has something similar. But I can see the benefit of it for all classes, even service classes, not just records. (In Kotlin, it's used for service classes all the time.) There's already been an RFC for clone-with that works on any object; it just never made it to a vote. I could see an argument for an even more dedicated syntax (eg, eliminate "clone" and just do "$foo with (bar: 'baz')"), but again, useful on all objects, not just records. The alternate creation syntax... OK, this one I can't really see a benefit to, and Ilija already noted it may cause conflicts. Value-based strong equality, in cases where I want that, I also want to be able to control it. That goes back to Jordan's operator overload RFC, and specifying a custom == and <=>. I'd rather just have that. Value-style passing is the really interesting one, but I want to be able to use it without being forced into all of the other features here. Eg, I could easily see wanting to have a value-style-passing mutable object. I do all kinds of in-place mutation in my function, then pass it on to something else, and because that's a function boundary it gets cloned (either immediately or later on modification) automatically for me. So what I see here is 4 different RFCs (value-passing, inline constructors, evolvable syntax, more robust object equality) that should stand on their own, for any object, so that I can pick and choose which I want a la carte. Giving me an fixed combination of them I cannot modify is not helpful. I would probably support all four of those as stand-alone RFCs (I'm still undecided about inline constructors, but could be talked into them). Plus, having another fixed type creates questions any time a new feature is added. Not all object features are available for Records.. So if we add a new object feature, should Records get it? Eg, the RFC has several very good examples of leveraging property hooks in a Record. Suppose that Records existed first, before hooks were introduced. Then we have to debate "so do hooks make sense on Records, too?" (And you know that bikeshed would be polkadotted by the time we're done.) For that matter, can I specify asymmetric visibility on a Record? Do we even want to have that debate? (I don't, honestly.) I would far prefer assembling record-ish behavior myself, using the smaller parts above. Eg: final readonly data class Point(int $x, int $y); "final" prevents extension. "readonly" makes it immutable. "data" gives it value-passing semantics. Any class can use an inline constructor. "with" is designed to work automatically on all objects. Boom, I've just assembled a Record out of its constituent parts, which also makes it easier for others to learn what I'm doing, because the features opted-in to are explicit, not implicit. So I don't think I can support a fixed bundle like this, but I would happily support the individual constituent features on their own. --Larry Garfield