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

Reply via email to