Re: RFC: Records

From: Date: Sun, 17 Nov 2024 21:30:24 +0000
Subject: Re: RFC: Records
References: 1 2  Groups: php.internals 
Request: Send a blank email to [email protected] to get a copy of this message
Hello Ilija and Larry,

You both touch upon some interesting and similar thoughts, and it may be worth sharing how I arrived
here; at least so we have some shared context:

Personally, I feel that classes are quite bloated with features and what feature works with what is
quite confusing for new developers; I would rather see features removed than added to them at this
point. This isn't why I chose the "record" keyword or a new syntax, to be clear.
However, it was one of many strong reasons as to why I felt it would be an ok deviation from
"tacking on more features to classes."

One of the main reasons for the alternative creation syntax is because I felt that "new"
was misleading at best, and just plain wrong at worst. It is also why I chose "&", to
make it clear you are not getting "a new one" but one that "just happens to exist
with the values you asked for." I'd be open to a different keyword or something else
entirely. It's just that "new" is the wrong one for records.

I did explore "data classes" to a degree, and I can see why Ilija created the new
"mutating" syntax for their RFC. It gets really weird, really fast. Records are the other
side of structs, though. They are (nearly literally) arrays with a class entry on them, and thus,
essentially, typed structured arrays with behavior. In fact, I was originally going to pitch it as
such, but decided it would either stand on its own or not; but it is what they were designed to be
from the beginning; hence the short declaration syntax.

In other words, I see this being used anywhere you'd normally use a structured array but want
some type safety, without all the boilerplate of classes or dealing with equality. For example,
DTO's, configuration, etc. They solve a different problem than Ilija's structs, which
makes more sense for collections, but I see records as being good candidates for values in those
collections.

As I shopped this RFC around to coworkers, old coworkers, other maintainers on the projects I work
on, and (nearly) random strangers on the internet, it became clear that people liked it and
understood them, but wanted more power. Things like better custom (de)serialization than what we
have with readonly classes, custom initializers for computed properties, etc. 

Furthermore, as I thought about these new features for records, I also thought about how people
would use them. They would most likely start with a simple record (or not), but when changing them,
I also thought about the diffs they would create in code reviews. In fact, this is a large reason
behind the rules for traditional constructors. Adding a traditional constructor to an already
existing record should look exactly like that in the diff, without shifting around a bunch of
properties.

Eventually, we ended up with the RFC you are reading today.

(To be 100% transparent, some people also thought it was pointless and wondered what was wrong with
readonly classes, but I'll come back to that).

Now, with that context in mind...

On Sun, Nov 17, 2024, at 08:21, Larry Garfield wrote:
> Plus, having another fixed type creates questions any time a new feature is added.

I think this would happen even if I scoped out some of the RFCs you hinted at. I don't think
there is any way around this. Adding new features complicates future features, and it doesn't
matter where you put them. Or rather, it matters, but not as much as you'd think.

For example, my nameof RFC is basically on hiatus until we have pattern matching. We simply lack the
grammar necessary to do it correctly, and it doesn't seem like the place to define that grammar
because then it may pigeonhole pattern matching. I think that is fine, but these are normal things
to go through when working on an RFC, in my limited experience.

My point is, we constantly have to make this decision process, and we just have to 'figure it
out' as we go.

On Sun, Nov 17, 2024, at 01:15, Ilija Tovilo wrote:
> I personally do not think immutable data structures are a good
> solution to the problem, and I don't feel like we need another,
> slightly shorter way to do the same thing.

On Sun, Nov 17, 2024, at 08:21, Larry Garfield wrote:
> 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)

To be clear, this is another tool in the developer's toolbox and not meant to replace classes
(even readonly classes). They are strictly immutable value objects, which makes sense for numbers,
time, custom values (see: the UserId example in the RFC), etc. These aren't as good for generic
collections like maps, sets, vectors, etc. or even things like services or controllers. For these
types of things, classes (or structs) make more sense.

Like Ilija mentioned in their email, there are significant performance optimizations to be had here
that are simply not possible using regular (readonly) classes. I didn't go into detail as to
how it works because it feels like an implementation detail, but I will spend some time distilling
this and its consequences, into the RFC, over the coming days. As a simple illustration, there can
be significant memory usage improvements:

100,000 arrays: https://3v4l.org/Z4CcV
100,000 readonly classes: https://3v4l.org/1vhNp

and what we would expect from records: https://3v4l.org/4nYXG
which is on par with the array example.

Naturally, real life won't see that kind of reduction in memory consumption (nobody is creating
an array of all the same items, most of the time), but hopefully it gives you an idea of what can be
gained.

On Sun, Nov 17, 2024, at 08:21, Larry Garfield wrote:
> 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.)

I think this would be a good future scope. If people like it, an RFC to extend it to regular classes
makes sense.

> 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.

I feel like this is similar to my nameof RFC and doomed from the start. Classes are just too
featureful to pin down what a "with" actually means.. The discussion went into depth on
this, and nothing definitive came out of it (IMHO). In a sense, records get a "fresh
start" and can define exactly what it means and how it can be used.

> The alternate creation syntax... OK, this one I can't really see a benefit to, and Ilija
> already noted it may cause conflicts.

And from Ilija:
> A small note: The $test = &Test(); syntax is ambiguous, as it's
> already legal. https://3v4l.org/CE5rt

I originally had something about this in the RFC but removed it at the last minute. I'll add it
back! For now, it will live in "open issues" in case someone has a better idea. As to the
reason for it, I covered it briefly above, but as a reminder, it boils down to my reluctance to use
"new" to get a record. If anyone has any better ideas, I'm open to it.

And again, from Larry:
> 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.

I originally wanted to add operators. For (I hope) obvious reasons, I decided to just wait for them.
I think something like records (with value semantics built-in) would be much more palatable for
people worried about the "abuse" of operators. I'm just going to steer clear of that
topic and leave it "undefined" for now--with the expectation that someone (Jordan?) may
come along to define operations on objects.

> 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.

I don't think there is much way around it. You either need a special syntax (structs) or
immutability (records). For regular classes, there is always "==" if you have control over
how they are compared. Sadly, I don't think this is possible for regular classes, but I could
be wrong.

And... that's a book. I'm really sorry about the length of this email, but hopefully
I've addressed both your questions and concerns as best I can.

Sincerely,

— Rob


Thread (9 messages)

« previous php.internals (#125978) next »