Skip to content

Commit 6cd68e6

Browse files
committed
updated rfc
1 parent c6cbaf0 commit 6cd68e6

File tree

1 file changed

+106
-68
lines changed

1 file changed

+106
-68
lines changed

published/records.ptxt

Lines changed: 106 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This RFC proposes the introduction of ''%%record%%'' objects, which are immutabl
1212

1313
==== Value objects ====
1414

15-
Value objects are immutable objects that represent a value. They’re used to store values with a different semantic meaning than their technical value, adding additional context. For example, a ''%%Point%%'' object with ''%%x%%'' and ''%%y%%'' properties can represent a point in a 2D space, and an ''%%ExpirationDate%%'' can represent a date when something expires. This prevents developers from accidentally using the wrong value in the wrong context.
15+
Value objects are immutable objects that represent a value. They’re used to store values with a different semantic by wrapping their technical value, adding additional context. For example, a ''%%Point%%'' object with ''%%x%%'' and ''%%y%%'' properties can represent a point in a 2D space, and an ''%%ExpirationDate%%'' can represent a date when something expires. This prevents developers from accidentally using the wrong value in the wrong context.
1616

1717
Consider this example where a function accepts an integer as a user ID, and the ID is accidentally set to a nonsensical value:
1818

@@ -76,6 +76,8 @@ A **record** body may also declare properties whose values are only mutable duri
7676

7777
A **record** body may also contain static methods and properties, which behave identically to static methods and properties in classes. They may be accessed using the ''%%::%%'' operator.
7878

79+
As an example, the following code defines a **record** named ''%%Pigment%%'' to represent a color, ''%%StockPaint%%'' to represent paint colors in stock, and ''%%PaintBucket%%'' to represent a collection of stock paints mixed together. The actual behavior isn’t important, but illustrates the syntax and semantics of records.
80+
7981
<code php>
8082
namespace Paint;
8183

@@ -125,7 +127,26 @@ record PaintBucket(StockPaint ...$constituents) {
125127

126128
=== Usage ===
127129

128-
A record may be used as a readonly class, as the behavior of the two is very similar, assisting in migrating from one implementation to another.
130+
A record may be used much like a class, as the behavior of the two is very similar, assisting in migrating from one implementation to another:
131+
132+
<code php>
133+
$gray = $bucket->mixIn($blackPaint)->mixIn($whitePaint);
134+
</code>
135+
136+
Records are instantiated in a function format, with ''%%&%%'' prepended. This provides visual feedback that a record is being created instead of a function call.
137+
138+
<code php>
139+
$black = &Pigment(0, 0, 0);
140+
$white = &Pigment(255, 255, 255);
141+
$blackPaint = &StockPaint($black, 1);
142+
$whitePaint = &StockPaint($white, 1);
143+
$bucket = &PaintBucket();
144+
145+
$gray = $bucket->mixIn($blackPaint)->mixIn($whitePaint);
146+
$grey = $bucket->mixIn($blackPaint)->mixIn($whitePaint);
147+
148+
assert($gray === $grey); // true
149+
</code>
129150

130151
=== Optional parameters and default values ===
131152

@@ -135,7 +156,7 @@ One or more properties defined in the inline constructor may have a default valu
135156

136157
<code php>
137158
record Rectangle(int $x, int $y = 10);
138-
var_dump(Rectangle(10)); // output a record with x: 10 and y: 10
159+
var_dump(&Rectangle(10)); // output a record with x: 10 and y: 10
139160
</code>
140161

141162
=== Auto-generated with method ===
@@ -163,7 +184,7 @@ record UserId(int $id) {
163184
}
164185
}
165186

166-
$userId = UserId(1);
187+
$userId = &UserId(1);
167188
$otherId = $userId->with(2); // Fails: Named arguments must be used
168189
$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not defined in the inline constructor
169190
$otherId = $userId->with(id: 2); // Success: id is updated
@@ -174,20 +195,15 @@ Using variadic arguments:
174195
<code php>
175196
record Vector(int $dimensions, int ...$values);
176197

177-
$vector = Vector(3, 1, 2, 3);
198+
$vector = &Vector(3, 1, 2, 3);
178199
$vector = $vector->with(dimensions: 4); // Success: values are updated
179200
$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: mixing named arguments with variadic arguments is not allowed by PHP syntax
180201
$vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // Success: First update dimensions, then values
181202
</code>
182203

183204
== Custom with method ==
184205

185-
A developer may define their own ''%%with%%'' method if they choose, and reference the generated ''%%with%%'' method using ''%%parent::with()%%''. This allows a developer to define policies or constraints on how data is updated.
186-
187-
Contravariance and covariance are enforced in the developer’s code via the ''%%Record%%'' interface:
188-
189-
* Contravariance: the parameter type of the custom ''%%with%%'' method must be a supertype of the generated ''%%with%%'' method.
190-
* Covariance: the return type of the custom ''%%with%%'' method must be ''%%self%%'' of the generated ''%%with%%'' method.
206+
A developer may define their own ''%%with%%'' method if they choose, and reference the generated ''%%with%%'' method using ''%%parent::with()%%''. This allows a developer to define policies or constraints on how data can change from instance to instance.
191207

192208
<code php>
193209
record Planet(string $name, int $population) {
@@ -212,43 +228,57 @@ The inline constructor is always required and must define at least one parameter
212228
When a traditional constructor exists and is called, the properties are already initialized to the values from the inline constructor and are mutable until the end of the method, at which point they become immutable.
213229

214230
<code php>
215-
// Inline constructor
216-
record User(string $name, string $email) {
231+
// Inline constructor defining two properties
232+
record User(string $name, string $emailAddress) {
217233
public string $id;
218234

219235
// Traditional constructor
220236
public function __construct() {
221-
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
237+
if (!is_valid_email($this->emailAddress)) {
222238
throw new InvalidArgumentException("Invalid email address");
223239
}
224240

225-
$this->id = hash('sha256', $email);
226-
$this->name = ucwords($name);
241+
$this->id = hash('sha256', $this->emailAddress);
242+
$this->name = ucwords($this->name);
243+
// all properties are now immutable
227244
}
228245
}
229246
</code>
230247

231248
==== Mental models and how it works ====
232249

233-
From the perspective of a developer, declaring a record declares an object and function with the same name. The developer can consider the record function (the inline constructor) as a factory function that creates a new object or retrieves an existing object from an array.
250+
From the perspective of a developer, declaring a record declares an object with the same name. The developer can consider the record function (the inline constructor) as a factory function that creates a new object or retrieves an existing object from an array.
234251

235252
For example, this would be a valid mental model for a Point record:
236253

237254
<code php>
238255
record Point(int $x, int $y) {
256+
public float $magnitude;
257+
258+
public function __construct() {
259+
$this->magnitude = sqrt($this->x ** 2 + $this->y ** 2);
260+
}
261+
239262
public function add(Point $point): Point {
240-
return Point($this->x + $point->x, $this->y + $point->y);
263+
return &Point($this->x + $point->x, $this->y + $point->y);
264+
}
265+
266+
public function dot(Point $point): int {
267+
return $this->x * $point->x + $this->y * $point->y;
241268
}
242269
}
243270

244271
// similar to declaring the following function and class
245272

246-
// used during construction to allow immutability
273+
// used during construction to allow mutability
247274
class Point_Implementation {
248275
public int $x;
249276
public int $y;
277+
public float $magnitude;
250278

251-
public function __construct() {}
279+
public function __construct() {
280+
$this->magnitude = sqrt($this->x ** 2 + $this->y ** 2);
281+
}
252282

253283
public function with(...$parameters) {
254284
// validity checks omitted for brevity
@@ -259,14 +289,16 @@ class Point_Implementation {
259289
public function add(Point $point): Point {
260290
return Point($this->x + $point->x, $this->y + $point->y);
261291
}
292+
293+
public function dot(Point $point): int {
294+
return $this->x * $point->x + $this->y * $point->y;
295+
}
262296
}
263297

264-
interface Record {
265-
public function with(...$parameters): self;
266-
}
298+
// used to enforce immutability but has nearly the same implementation
299+
readonly class Point {
300+
public float $magnitude;
267301

268-
// used to enforce immutability but has the same implementation
269-
readonly class Point implements Record {
270302
public function __construct(public int $x, public int $y) {}
271303

272304
public function with(...$parameters): self {
@@ -278,64 +310,73 @@ readonly class Point implements Record {
278310
public function add(Point $point): Point {
279311
return Point($this->x + $point->x, $this->y + $point->y);
280312
}
313+
314+
public function dot(Point $point): int {
315+
return $this->x * $point->x + $this->y * $point->y;
316+
}
281317
}
282318

283319
function Point(int $x, int $y): Point {
284320
static $points = [];
285-
// look up the identity of the point
286-
$key = hash_func($x, $y);
321+
322+
$key = hash_object($mutablePoint);
287323
if ($points[$key] ?? null) {
288324
// return an existing point
289325
return $points[$key];
290326
}
291-
327+
292328
// create a new point
293329
$reflector = new \ReflectionClass(Point_Implementation::class);
294-
$point = $reflector->newInstanceWithoutConstructor();
295-
$point->x = $x;
296-
$point->y = $y;
297-
$point->__construct();
298-
// copy properties to an immutable point and return it
299-
$point = new Point($point->x, $point->y);
330+
$mutablePoint = $reflector->newInstanceWithoutConstructor();
331+
$mutablePoint->x = $x;
332+
$mutablePoint->y = $y;
333+
$mutablePoint->__construct();
334+
335+
// copy properties to an immutable Point and return it
336+
$point = new Point($mutablePoint->x, $mutablePoint->y);
337+
$point->magnitude = $mutablePoint->magnitude;
300338
return $points[$key] = $point;
301339
}
302340
</code>
303341

304-
In reality, this is quite different from how it works in the engine, but this provides a mental model of how behavior should be expected to work. In other words, if it can work in the above model, then it be possible.
342+
In reality, this is quite different from how it works in the engine, but this provides a mental model of how behavior should be expected to work.
305343

306344
==== Performance considerations ====
307345

308-
To ensure that records are both performant and memory-efficient, the RFC proposes leveraging PHP’s copy-on-write (COW) semantics (similar to arrays) and interning values. Unlike interned strings, the garbage collector will be allowed to clean up these interned records when they’re no longer necessary.
346+
To ensure that records are both performant and memory-efficient, the RFC proposes leveraging PHP’s copy-on-write (COW) semantics (similar to arrays) and interning values. Unlike interned strings, the garbage collector will be allowed to clean up these interned records when they’re no longer referenced.
309347

310348
<code php>
311-
$point1 = Point(3, 4);
349+
$point1 = &Point(3, 4);
312350
$point2 = $point1; // No data duplication, $point2 references the same data as $point1
313351
$point3 = Point(3, 4); // No data duplication, it is pointing to the same memory as $point1
314352

315353
$point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance
354+
$point5 = &Point(5, 4); // No data duplication, it is pointing to the same memory as $point4
316355
</code>
317356

318357
=== Cloning and with() ===
319358

320359
Calling ''%%clone%%'' on a ''%%record%%'' results in the same record object being returned. As it is a "value" object, it represents a value and is the same thing as saying ''%%clone 3%%''—you expect to get back a ''%%3%%''.
321360

322-
''%%with%%'' may be called with no arguments, and it is the same behavior as ''%%clone%%''. This is an important consideration because a developer may call ''%%$new = $record->with(...$array)%%'' and we don’t want to crash. If a developer wants to crash, they can do by ''%%assert($new !== $record)%%''.
361+
If ''%%->with()%%'' is called with no arguments, a warning will be emitted, as this is most likely a mistake.
323362

324363
==== Serialization and deserialization ====
325364

326-
Records are fully serializable and deserializable.
365+
Records are fully serializable and deserializable, even when nested.
327366

328367
<code php>
329368
record Single(string $value);
330369
record Multiple(string $value1, string $value2);
331370

332-
echo $single = serialize(Single('value')); // Outputs: "O:6:"Single":1:{s:5:"value";s:5:"value";}"
333-
echo $multiple = serialize(Multiple('value1', 'value2')); // Outputs: "O:8:"Multiple":1:{s:6:"values";a:2:{i:0;s:6:"value1";i:1;s:6:"value2";}}"
371+
echo $single = serialize(&Single('value')); // Outputs: "O:6:"Single":1:{s:5:"value";s:5:"value";}"
372+
echo $multiple = serialize(&Multiple('value1', 'value2')); // Outputs: "O:8:"Multiple":1:{s:6:"values";a:2:{i:0;s:6:"value1";i:1;s:6:"value2";}}"
334373

335-
echo unserialize($single) === Single('value'); // Outputs: true
336-
echo unserialize($multiple) === Multiple('value1', 'value2'); // Outputs: true
374+
echo unserialize($single) === &Single('value'); // Outputs: true
375+
echo unserialize($multiple) === &Multiple('value1', 'value2'); // Outputs: true
337376
</code>
338377

378+
If a record contains objects or values that are unserializable, the record will not be serializable.
379+
339380
==== Equality ====
340381

341382
A ''%%record%%'' is always strongly equal (''%%===%%'') to another record with the same value in the properties, much like an ''%%array%%'' is strongly equal to another array containing the same elements. For all intents, ''%%$recordA === $recordB%%'' is the same as ''%%$recordA == $recordB%%''.
@@ -344,7 +385,7 @@ Comparison operations will behave exactly like they do for classes, which is cur
344385

345386
=== Non-trivial values ===
346387

347-
For non-trivial values (e.g., objects, closures, resources, etc.), the ''%%===%%'' operator will return ''%%true%%'' if the two operands reference the same object.
388+
For non-trivial values (e.g., objects, closures, resources, etc.), the ''%%===%%'' operator will return ''%%true%%'' if the two operands reference the same instances.
348389

349390
For example, if two different DateTime records reference the exact same date and are stored in a record, the records will not be considered equal:
350391

@@ -360,7 +401,7 @@ $dateRecord2 = Date($date2);
360401
echo $dateRecord1 === $dateRecord2; // Outputs: false
361402
</code>
362403

363-
However, this can be worked around by being a bit creative (see: mental model):
404+
However, this can be worked around by being a bit creative (see: mental model) as only the values passed in the constructor are compared:
364405

365406
<code php>
366407
record Date(string $date) {
@@ -371,10 +412,10 @@ record Date(string $date) {
371412
}
372413
}
373414

374-
$date1 = Date('2024-07-19');
375-
$date2 = Date('2024-07-19');
415+
$date1 = &Date('2024-07-19');
416+
$date2 = &Date('2024-07-19');
376417

377-
echo $date1->datetime === $date2->datetime; // Outputs: true
418+
echo $date1->datetime === $date2->datetime ? 'true' : 'false'; // Outputs: true
378419
</code>
379420

380421
==== Type hinting ====
@@ -412,21 +453,25 @@ Calling ''%%finalizeRecord()%%'' on a record that has already been finalized wil
412453

413454
=== isRecord() ===
414455

415-
The ''%%isRecord()%%'' method is used to determine if an object is a record. It returns ''%%true%%'' if the object is a record,
456+
The ''%%isRecord()%%'' method is used to determine if an object is a record. It returns ''%%true%%'' if the object is a record.
416457

417458
=== getInlineConstructor() ===
418459

419460
The ''%%getInlineConstructor()%%'' method is used to get the inline constructor of a record as a ''%%ReflectionFunction%%''. This can be used to inspect inlined properties and their types.
420461

462+
Invoking the ''%%invoke()%%'' method on the ''%%ReflectionFunction%%'' will create a finalized record.
463+
421464
=== getTraditionalConstructor() ===
422465

423466
The ''%%getTraditionalConstructor()%%'' method is used to get the traditional constructor of a record as a ''%%ReflectionMethod%%''. This can be useful to inspect the constructor for further initialization.
424467

468+
Invoking the ''%%invoke()%%'' method on the ''%%ReflectionMethod%%'' on a finalized record will throw an exception.
469+
425470
=== makeMutable() ===
426471

427472
The ''%%makeMutable()%%'' method is used to create a new instance of a record with mutable properties. The returned instance doesn’t provide any value semantics and should only be used for testing purposes or when there is no other option.
428473

429-
A mutable record can be finalized again using ''%%finalizeRecord()%%'' and to the engine, these are regular classes. For example, ''%%var_dump()%%'' will output ''%%object%%'' instead of ''%%record%%''.
474+
A mutable record can be finalized again using ''%%finalizeRecord()%%''. A mutable record will not be considered a record by ''%%isRecord()%%'' or implement the ''%%\Record%%'' interface. It is a regular object with the same properties and methods as the record. For example, ''%%var_dump()%%'' will output ''%%object%%'' instead of ''%%record%%''.
430475

431476
=== isMutable() ===
432477

@@ -439,10 +484,10 @@ In cases where custom deserialization is required, a developer can use ''%%Refle
439484
<code php>
440485
record Seconds(int $seconds);
441486

442-
$example = Seconds(5);
487+
$example = &Seconds(5);
443488

444-
$reflector = new ReflectionRecord(ExpirationDate::class);
445-
$expiration = $reflector->newInstanceWithoutConstructor();
489+
$reflector = new ReflectionRecord(Seconds::class);
490+
$expiration = $reflector->newInstanceWithoutConstructor(); // this is a mutable object
446491
$expiration->seconds = 5;
447492
assert($example !== $expiration); // true
448493
$expiration = $reflector->finalizeRecord($expiration);
@@ -464,20 +509,18 @@ record(Point)#1 (2) {
464509

465510
==== Considerations for implementations ====
466511

467-
A ''%%record%%'' cannot share its name with an existing ''%%record%%'', ''%%class%%'', or ''%%function%%'' because defining a ''%%record%%'' creates both a ''%%class%%'' and a ''%%function%%'' with the same name.
512+
A ''%%record%%'' cannot share its name with an existing ''%%record%%'', ''%%class%%'', ''%%interface%%'', ''%%trait%%'', or ''%%function%%'', just like a class.
468513

469514
==== Autoloading ====
470515

471-
This RFC chooses to omit autoloading from the specification for a record. The reason is that instantiating a record calls the function implicitly declared when the record is explicitly declared, PHP doesn’t currently support autoloading functions, and solving function autoloading is out-of-scope for this RFC.
472-
473-
Once function autoloading is implemented in PHP at some hopeful point in the future, said autoloader could locate the record and then autoload it.
474-
475-
The author of this RFC strongly encourages someone to put forward a function autoloading RFC if autoloading is desired for records.
516+
Records will be autoloaded in the same way as classes.
476517

477518
===== Backward Incompatible Changes =====
478519

479520
To avoid conflicts with existing code, the ''%%record%%'' keyword will be handled similarly to ''%%enum%%'' to prevent backward compatibility issues.
480521

522+
Since ''%%&%%'' is currently a syntax error when prefixed on a function call, it will be used to denote a record instantiation.
523+
481524
===== Proposed PHP Version(s) =====
482525

483526
PHP 8.5
@@ -518,25 +561,20 @@ None.
518561

519562
===== Proposed Voting Choices =====
520563

521-
Include these so readers know where you’re heading and can discuss the proposed voting options.
564+
2/3 majority.
522565

523566
===== Patches and Tests =====
524567

525568
TBD
526569

527570
===== Implementation =====
528571

529-
After the project is implemented, this section should contain
530-
531-
- the version(s) it was merged into
532-
- a link to the git commit(s)
533-
- a link to the PHP manual entry for the feature
534-
- a link to the language specification section (if any)
572+
To be completed during a later phase of discussion.
535573

536574
===== References =====
537575

538-
Links to external references, discussions or RFCs
576+
* [[https://en.wikipedia.org/wiki/Value_semantics|Value semantics]]
539577

540578
===== Rejected Features =====
541579

542-
Keep this updated with features that were discussed on the mail lists.
580+
TBD

0 commit comments

Comments
 (0)