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
Hello internals,
I'm ready as I'm going to be to introduce to you: "Records" https://wiki.php.net/rfc/records!
Thanks for your proposal. I very much agree that value semantics are a
highly desirable feature. I sent out my concept proposal on how this
may be achieved through structs some months ago. [1] There are some
remaining technical challenges, but the PoC looks promising. [2]
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. In my proposal, I mentioned
that there are two primary issues with immutable data structures:
- They are expensive. Any modification of the class requires a clone
of the object by design, even when the cloned object is immediately
discarded. This is most noticeable for lists and other growable data
types, because they are large and change frequently. An O(n) insertion
becomes infeasible very fast. - The clone is explicit, which gets old pretty quickly. Many of PHPs
quality-of-life features like op-assign (e.g. +=) become unusable.
Structs would instead be fully-fledged objects that adopt CoW
(copy-on-write) semantics, which is how PHP already implements arrays.
Essentially, objects are automatically cloned when they are changed
and when the object is referenced from multiple places. If the
object is not referenced anywhere else, it may be safely mutated
without a clone, as there is nobody to observe this side-effect. When
a shared object is mutated multiple times (e.g. an append to a list in
a loop), only the first mutation requires a clone. It's also something
you don't need to think about, it just happens.
As mentioned, the main motivation of my proposal is to implement new
dedicated data structures like Vectors, Maps, Sets, etc. in PHP
(userland and extensions) without sacrificing ergonomics or
performance. However, they would also solve the same challenges for
simple data objects. IMO, immutability was never a great solution to
the problem of "spooky action at a distance". Mutability itself is not
a problem, just shared mutability.
Note that this concept is heavily inspired by Swift, Rust and likely
other languages I don't know. Chris Lattner (the original author of
LLVM and Swift) has shared very similar thoughts in an interview
recently. [3]
If the primary motivation of your RFC is to reduce boilerplate, then
it's true that structs would not help much. I do wonder whether 1. we
need even shorter code and 2. if we do, whether it's something that
could be added to classes alike, rather than tying it to records when
it could be more generic instead.
A small note: The $test = &Test(); syntax is ambiguous, as it's
already legal. https://3v4l.org/CE5rt
Ilija
[1] https://externals.io/message/122845
[2] https://github.com/php/php-src/pull/13800
[3] https://youtu.be/JRcXUuQYR90?si=p51_x3wkfeeeGfq-&t=3319
Hi Rob and Ilia
Thank you both for your detailed proposals and the efforts you put into
shaping this important feature for PHP It is clear a lot of thought has
gone into both approaches and I appreciate the opportunity to share my
thoughts.
niedz., 17 lis 2024 o 01:17 Ilija Tovilo tovilo.ilija@gmail.com
napisał(a):
Hi Rob
Hello internals,
I'm ready as I'm going to be to introduce to you: "Records"
https://wiki.php.net/rfc/records!Thanks for your proposal. I very much agree that value semantics are a
highly desirable feature. I sent out my concept proposal on how this
may be achieved through structs some months ago. [1] There are some
remaining technical challenges, but the PoC looks promising. [2]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. In my proposal, I mentioned
that there are two primary issues with immutable data structures:
- They are expensive. Any modification of the class requires a clone
of the object by design, even when the cloned object is immediately
discarded. This is most noticeable for lists and other growable data
types, because they are large and change frequently. An O(n) insertion
becomes infeasible very fast.- The clone is explicit, which gets old pretty quickly. Many of PHPs
quality-of-life features like op-assign (e.g. +=) become unusable.Structs would instead be fully-fledged objects that adopt CoW
(copy-on-write) semantics, which is how PHP already implements arrays.
Essentially, objects are automatically cloned when they are changed
and when the object is referenced from multiple places. If the
object is not referenced anywhere else, it may be safely mutated
without a clone, as there is nobody to observe this side-effect. When
a shared object is mutated multiple times (e.g. an append to a list in
a loop), only the first mutation requires a clone. It's also something
you don't need to think about, it just happens.As mentioned, the main motivation of my proposal is to implement new
dedicated data structures like Vectors, Maps, Sets, etc. in PHP
(userland and extensions) without sacrificing ergonomics or
performance. However, they would also solve the same challenges for
simple data objects. IMO, immutability was never a great solution to
the problem of "spooky action at a distance". Mutability itself is not
a problem, just shared mutability.Note that this concept is heavily inspired by Swift, Rust and likely
other languages I don't know. Chris Lattner (the original author of
LLVM and Swift) has shared very similar thoughts in an interview
recently. [3]If the primary motivation of your RFC is to reduce boilerplate, then
it's true that structs would not help much. I do wonder whether 1. we
need even shorter code and 2. if we do, whether it's something that
could be added to classes alike, rather than tying it to records when
it could be more generic instead.A small note: The $test = &Test(); syntax is ambiguous, as it's
already legal. https://3v4l.org/CE5rtIlija
[1] https://externals.io/message/122845
[2] https://github.com/php/php-src/pull/13800
[3] https://youtu.be/JRcXUuQYR90?si=p51_x3wkfeeeGfq-&t=3319
The goal of Records as I see it is to provide a concise syntax for defining
immutable value objects optimized for simplicity and common use cases
and I strongly align with this goal. The concise syntax in your proposal is
a significant advantage for small immutable types
record User(string $name, int $age)
This simplicity eliminates boilerplate making it an excellent fit for
common scenarios like DTOs or configuration objects
Structs are designed to provide immutability with Copy-on-Write semantics
ensuring efficiency when dealing with large or nested structures
While I prefer Rob’s syntax Ilia’s focus on performance through
Copy-on-Write is compelling. For example CoW optimization could make
scenarios like this more efficient
struct Line {
public Point $start
public Point $end
}
$line1 = new Line(start: new Point(0, 0), end: new Point(1, 1))
$line2 = $line1->with(end: new Point(2, 2))
Could CoW be integrated into Records for performance-critical cases without
sacrificing simplicity?
On Rob’s Records proposal the Syntax Simplicity and Automatic Equality make
equality straightforward and intuitive
$user1 = new User("Alice", 30)
$user2 = new User("Alice", 30)
var_dump($user1 === $user2) // true
On Ilia’s Structs proposal Copy-on-Write Semantics offers performance
advantages for large or nested structures making it a great fit for
optimizing immutability but the Explicit Equality - while useful for
complex cases explicit equality checks introduce additional boilerplate for
simple scenarios.
Rob’s Records proposal is exceptionally well-suited for lightweight
immutable types and aligns with PHP’s evolution toward simplicity and
developer experience. That said, Ilia's CoW optimizations are valuable for
performance and could complement Records in specific cases.
Thank you both again for your hard work and thoughtful proposals. To move
this discussion forward perhaps we could organize a community vote to gauge
which capabilities are preferred overall. This could help the team better
understand the direction the community supports and explore possible hybrid
solutions if needed.
On a related note I was recently asked about my old draft RFC proposal for
Structs[1] and whether it was abandoned or forgotten. While it didn’t
address many of the ideas you have both raised, it was built with
composition in mind from the start. I hope that such capabilities could
also be explored in future extensions as they would bring additional
flexibility to these immutable types.
[1] https://wiki.php.net/rfc/structs#fields_composition
Best regards
Michał Marcin Brzuchalski
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); // trueRecords 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
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...
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.
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.
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.
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
Hi Rob,
I'm torn on this one. On the one hand, it does look like a nice solution
for adding custom value objects to the language; on the other hand, it's
a lot of things that are "just slightly different" for users to get used to.
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'm not convinced by this, because I'm not convinced the "mental model"
in the RFC is the one that most developers need to care about. I think a
much simpler mental model (for someone who understands the rest of PHP) is:
- records are copy-on-write (like arrays, not like objects)
- the === operator returns true for two records with the same value
(again like arrays, not like objects) - the implementation optimises two records with the same values to share
memory
The third point is useful to know if you're creating a lot of them, but
probably irrelevant most of the time.
We also might not want to make it a hard guarantee, because there may be
cases where a different trade-off is more efficient. For instance, maybe
we will optimise $foo->with(a: 1)->with(b: 2)->with(c: 3) to overwrite
values in-place, at the cost of an extra condition in the ===
implementation.
That would be similar to some of the changes to zvals in PHP 7; for
instance, "$foo=42; $bar=$foo;" will copy the value 42 to a new piece of
memory, not increase a zval reference count as PHP 5 would have done.
That leaves the mental model as mostly "records are a bit like arrays".
Now, it's true that we don't write "new array(1,2,3)"; but I have heard
people calling the "array()" syntax "the array constructor", and the
manual describes it as "creating an array". Similarly, you use
"constructor" and "construction" throughout the RFC.
All of that makes it feel perfectly natural for me to have "new Point(1,
5)" mean "create a Point record; feel free to save memory by reusing one
with the same values".
A record may contain a traditional constructor with zero arguments
to perform further initialization.
A record body may also declare properties whose values are only
mutable during a constructor call. At any other time, the property is
immutable.
Talking of constructors, I find the proposed syntax rather confusing,
because it's doing the same job as constructor property promotion, but
in almost the opposite way: taking things out of the constructor
signature, vs putting them in:
readonly class Foo {
public string $bytes;
public function __construct(public int $len) {
$this->bytes = random_bytes($this->len);
}
}
record Foo (int $len) {
public string $bytes;
public function __construct() {
$this->bytes = random_bytes($this->len);
}
}
While writing this example, I realised that the behaviour is also
confusing: when exactly will the constructor be called? Consider:
$a = &Foo(42); // new Record; constructor called
$b = &Foo(42); // re-use cached Record; is the constructor skipped? or
called, but the result discarded? what if the constructor modifies
$this->len?
unset($a, $b);
$c = &Foo(42); // does this re-use the Record? or has it been garbage
collected, so this will call the constructor again?
I think we should decide between two paths:
- structs/records as a special kind of object, keeping as much behaviour
and syntax from classes as we can; that means no "inline constructor",
and probably no "pull a cached instance from memory" - structs/records as a brand new thing, with new syntax that only allows
the parts that fit the model; that means no non-constructor properties,
and no constructor bodies
Regards,
--
Rowan Tommins
[IMSoP]
Hi Larry,
niedz., 17 lis 2024 o 08:24 Larry Garfield larry@garfieldtech.com
napisał(a):
...
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.)
What visibility would you expect for inline constructor properties in
service classes?
For records it is clearly a public fields declaration, while for service
classes I'd expect private|protected - adding visibility increases the
boilerplate back to normal classes.
The alternate creation syntax... OK, this one I can't really see a benefit
to, and Ilija already noted it may cause conflicts.
I agree, personally I'd see "new" keyword used like for normal classes
still feasible.
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.
It opens a bunch of concerns, questions like why would you use "data"
keyword without having "final"?
Although I like the "data" keyword very much.
Kind regards,
Michał Marcin Brzuchalski
Hi Larry,
niedz., 17 lis 2024 o 08:24 Larry Garfield larry@garfieldtech.com napisał(a):
...
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.)What visibility would you expect for inline constructor properties in
service classes?
Good question. If we follow Kotlin's lead[1] (Kotlin has like 3-4 different places to do constructor-ish logic, it's kinda weird), then they'd default to public, but you can optionally specify a visibility. So, the same as any other property. They do not imply readonly, but Kotlin has per-arg val/var markers on everything anyway to handle that.
[1] https://kotlinlang.org/docs/classes.html#constructors
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.
It opens a bunch of concerns, questions like why would you use "data"
keyword without having "final"?
Although I like the "data" keyword very much.
To borrow a little syntax from Kotlin again, and randomly spitball:
data class Rectangle(int $h, int $w) {
public int $area { get => $this->h * $this->w; }
}
data class Square(int $side) extends Rectangle($side, $side);
(That calls the constructor of Rectangle with the provided values.)
I'm generally not in favor of final-by-default. final has its place, certainly, but I am firmly on team "no, you should not be making every class final by default, that's just silly."
Another thing to consider is ADTs. (cf https://wiki.php.net/rfc/tagged_unions, though the text there is quite old and out dated at this point; do not take literally.) The intent for enum cases with associated data was that you would specify properties and types, and those would be public-readonly-enforced. That's the only option. Ilija also had plans (and I think implementation, but I'm not sure) for reusing instances with matching properties so they would point to the same object in memory, making === work. So:
enum Move {
case Left(int $distance);
case Right(int $distance);
}
$step1 = Move::Left(5);
$step2 = Move::Left(5);
$step1 === $step2; // true
That sounds somewhat similar to the restrictions proposed for Records, though in context I think they make more sense on enums. I can see there being overlap, though, so I mention it here for completeness. I'm not sure how this all dovetails together.
To reiterate, my main issue with the Records concept is that it couples too many features together into an all-or-nothing package. readonly did that with only 2 features, and it caused Ilija and I a lot of heartburn and helped keep aviz from passing in 8.3. Records would couple 4 features together. Once bitten, twice as cautious.
--Larry Garfield
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.
Sorry for the radio silence, but I found myself saying something was impossible. This is software, anything is possible...
Anyway, I think I figured out how to implement a "data" modifier, and it is quite excellent (for the exact reasons you give).
I'll break this up into multiple RFCs, pending some further tests to try and break my proof-of-concept:
- a "data" modifier on class for value semantics
- an "inline constructor"
As you mention, at that point, a "record" or "value" keyword can just be shorthand for "final readonly data class".
— Rob