From 5e7319c2930cd7e11eed002ad1d264529f072cdc Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 14:03:10 +0200 Subject: [PATCH 01/46] initial commit --- drafts/records.md | 308 ++++++++++++++++++++++++++++++++++++++++++ published/records.txt | 251 ++++++++++++++++++++++++++++++++++ 2 files changed, 559 insertions(+) create mode 100644 drafts/records.md create mode 100644 published/records.txt diff --git a/drafts/records.md b/drafts/records.md new file mode 100644 index 0000000..5be28df --- /dev/null +++ b/drafts/records.md @@ -0,0 +1,308 @@ +# PHP RFC: Records + +- Version: 0.9 +- Date: 2024-07-19 +- Author: Robert Landers, landers.robert@gmail.com +- Status: Draft (or Under Discussion or Accepted or Declined) +- First Published at: + +## Introduction + +In modern PHP development, the need for concise and immutable data +structures is increasingly recognized. Inspired by the concept of +"records", "data objects", or "structs" in other languages, this RFC +proposes the addition of `record` objects in PHP. These records will +provide a concise and immutable data structure, distinct from `readonly` +classes, enabling developers to define immutable objects with less +boilerplate code. + +## Proposal + +This RFC proposes the introduction of a new record keyword in PHP to +define immutable data objects. These objects will allow properties to be +initialized concisely and will provide built-in methods for common +operations such as modifying properties and equality checks using a +function-like instantiation syntax. Records can implement interfaces and +use traits but cannot extend other records or classes. + +#### Syntax and semantics + +##### Definition + +A `record` is defined by the word "record", followed by the name of its +type, an open parenthesis containing zero or more typed parameters that +become public, immutable, properties. They may optionally implement an +interface using the `implements` keyword. A `record` body is optional. + +A `record` may NOT contain a constructor; instead of defining initial +values, property hooks should be used to produce computable values +on-demand. Defining a constructor emits a compilation error. + +A `record` body may contain property hooks, methods, and use traits. +Regular properties may also be defined, but they are immutable by +default and are no different than `const`. + +Static properties and methods are forbidden in a `record` (this includes +`const`, a regular property may be used instead). Attempting to define +static properties, methods, constants results in a compilation error. + +``` php +namespace Geometry; + +interface Shape { + public function area(): float; +} + +trait Dimension { + public function dimensions(): array { + return [$this->width, $this->height]; + } +} + +record Vector2(int $x, int $y); + +record Rectangle(Vector2 $leftTop, Vector2 $rightBottom) implements Shape { + use Dimension; + + public int $height = 1; // this will always and forever be "1", it is immutable. + + public int $width { get => $this->rightBottom->x - $this->topLeft->x; } + public int $height { get => $this->rightBottom->y - $this->topLeft->y; } + + public function area(): float { + return $this->width * $this->height; + } +} +``` + +##### Usage + +A `record` may be used as a `readonly class`, as the behavior of it is +very similar with no key differences to assist in migration from +`readonly class`. + +``` php +$rect1 = Rectangle(Point(0, 0), Point(1, -1)); +$rect2 = $rect1->with(topLeft: Point(0, 1)); + +var_dump($rect2->dimensions()); +``` + +##### Optional parameters and default values + +A `record` can also be defined with optional parameters that are set if +left out during instantiation. + +``` php +record Rectangle(int $x, int $y = 10); +var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 +``` + +##### Auto-generated `with` method + +To enhance the usability of records, the RFC proposes automatically +generating a `with` method for each record. This method allows for +partial updates of properties, creating a new instance of the record +with the specified properties updated. + +The auto-generated `with` method accepts only named arguments defined in +the constructor. No other property names can be used, and it returns a +new record object with the given values. + +``` php +$point1 = Point(3, 4); +$point2 = $point1->with(x: 5); +$point3 = $point1->with(null, 10); // must use named arguments + +echo $point1->x; // Outputs: 3 +echo $point2->x; // Outputs: 5 +``` + +A developer may define their own `with` method if they so choose. + +#### Performance considerations + +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 +are no longer needed. + +``` php +$point1 = Point(3, 4); +$point2 = $point1; // No data duplication, $point2 references the same data as $point1 +$point3 = Point(3, 4); // No data duplication here either, it is pointing the the same memory as $point1 + +$point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance with modified data +``` + +##### Cloning and with() + +Calling `clone` on a `record` results in the exact 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`. + +`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)`. + +#### Equality + +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`. + +Comparison operations will behave exactly like they do for classes. + +### Reflection + +Records in PHP will be fully supported by the reflection API, providing +access to their properties and methods just like regular classes. +However, immutability and special instantiation rules will be enforced. + +#### ReflectionClass support + +`ReflectionClass` can be used to inspect records, their properties, and +methods. Any attempt to modify record properties via reflection will +throw an exception, maintaining immutability. Attempting to create a new +instance via `ReflectionClass` will cause a `ReflectionException` to be +thrown. + +``` php +$point = Point(3, 4); +$reflection = new \ReflectionClass($point); + +foreach ($reflection->getProperties() as $property) { + echo $property->getName() . ': ' . $property->getValue($point) . PHP_EOL; +} +``` + +#### Immutability enforcement + +Attempts to modify record properties via reflection will throw an +exception. + +``` php +try { + $property = $reflection->getProperty('x'); + $property->setValue($point, 10); // This will throw an exception +} catch (\ReflectionException $e) { + echo 'Exception: ' . $e->getMessage() . PHP_EOL; // "Cannot modify a record property" +} +``` + +#### ReflectionFunction for implicit constructor + +Using `ReflectionFunction` on a record will reflect the implicit +constructor. + +``` php +$constructor = new \ReflectionFunction('Geometry\Point'); +echo 'Constructor Parameters: '; +foreach ($constructor->getParameters() as $param) { + echo $param->getName() . ' '; +} +``` + +#### New functions and methods + +- Calling `is_object($record)` will return `true`. +- A new function, `is_record($record)`, will return `true` for records, + and `false` otherwise +- Calling `get_class($record)` will return the record name + +#### var_dump + +Calling `var_dump` will look much like it does for objects, but instead +of `object` it will say `record`. + + record(Point)#1 (2) { + ["x"]=> + int(1) + ["y"]=> + int(2) + } + +### Considerations for implementations + +A `record` cannot be named after an existing `record`, `class` or +`function`. This is because defining a `record` creates both a `class` +and a `function` with the same name. + +### Auto loading + +As invoking a record value by its name looks remarkably similar to +calling a function, and PHP has no function autoloader, auto loading +will not be supported in this implementation. If function auto loading +were to be implemented in the future, an autoloader could locate the +`record` and autoload it. The author of this RFC strongly encourages +someone to put forward a function auto loading RFC if auto loading is +desired for records. + +## Backward Incompatible Changes + +No backward incompatible changes. + +## Proposed PHP Version(s) + +PHP 8.5 + +## RFC Impact + +### To SAPIs + +N/A + +### To Existing Extensions + +N/A + +### To Opcache + +Unknown. + +### New Constants + +None + +### php.ini Defaults + +None + +## Open Issues + +Todo + +## Unaffected PHP Functionality + +None. + +## Future Scope + +## Proposed Voting Choices + +Include these so readers know where you are heading and can discuss the +proposed voting options. + +## Patches and Tests + +TBD + +## Implementation + +After the project is implemented, this section should contain + +1. the version(s) it was merged into +2. a link to the git commit(s) +3. a link to the PHP manual entry for the feature +4. a link to the language specification section (if any) + +## References + +Links to external references, discussions or RFCs + +## Rejected Features + +Keep this updated with features that were discussed on the mail lists. diff --git a/published/records.txt b/published/records.txt new file mode 100644 index 0000000..956fa61 --- /dev/null +++ b/published/records.txt @@ -0,0 +1,251 @@ +====== PHP RFC: Records ====== + + * Version: 0.9 + * Date: 2024-07-19 + * Author: Robert Landers, + * Status: Draft (or Under Discussion or Accepted or Declined) + * First Published at: http://wiki.php.net/rfc/records + +===== Introduction ===== + +In modern PHP development, the need for concise and immutable data structures is increasingly recognized. Inspired by the concept of "records", "data objects", or "structs" in other languages, this RFC proposes the addition of ''%%record%%'' objects in PHP. These records will provide a concise and immutable data structure, distinct from ''%%readonly%%'' classes, enabling developers to define immutable objects with less boilerplate code. + +===== Proposal ===== + +This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but cannot extend other records or classes. + +=== Syntax and semantics === + +== Definition == + +A ''%%record%%'' is defined by the word "record", followed by the name of its type, an open parenthesis containing zero or more typed parameters that become public, immutable, properties. They may optionally implement an interface using the ''%%implements%%'' keyword. A ''%%record%%'' body is optional. + +A ''%%record%%'' may NOT contain a constructor; instead of defining initial values, property hooks should be used to produce computable values on-demand. Defining a constructor emits a compilation error. + +A ''%%record%%'' body may contain property hooks, methods, and use traits. Regular properties may also be defined, but they are immutable by default and are no different than ''%%const%%''. + +Static properties and methods are forbidden in a ''%%record%%'' (this includes ''%%const%%'', a regular property may be used instead). Attempting to define static properties, methods, constants results in a compilation error. + + +namespace Geometry; + +interface Shape { + public function area(): float; +} + +trait Dimension { + public function dimensions(): array { + return [$this->width, $this->height]; + } +} + +record Vector2(int $x, int $y); + +record Rectangle(Vector2 $leftTop, Vector2 $rightBottom) implements Shape { + use Dimension; + + public int $height = 1; // this will always and forever be "1", it is immutable. + + public int $width { get => $this->rightBottom->x - $this->topLeft->x; } + public int $height { get => $this->rightBottom->y - $this->topLeft->y; } + + public function area(): float { + return $this->width * $this->height; + } +} + + +== Usage == + +A ''%%record%%'' may be used as a ''%%readonly class%%'', as the behavior of it is very similar with no key differences to assist in migration from ''%%readonly class%%''. + + +$rect1 = Rectangle(Point(0, 0), Point(1, -1)); +$rect2 = $rect1->with(topLeft: Point(0, 1)); + +var_dump($rect2->dimensions()); + + +== Optional parameters and default values == + +A ''%%record%%'' can also be defined with optional parameters that are set if left out during instantiation. + + +record Rectangle(int $x, int $y = 10); +var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 + + +== Auto-generated with method == + +To enhance the usability of records, the RFC proposes automatically generating a ''%%with%%'' method for each record. This method allows for partial updates of properties, creating a new instance of the record with the specified properties updated. + +The auto-generated ''%%with%%'' method accepts only named arguments defined in the constructor. No other property names can be used, and it returns a new record object with the given values. + + +$point1 = Point(3, 4); +$point2 = $point1->with(x: 5); +$point3 = $point1->with(null, 10); // must use named arguments + +echo $point1->x; // Outputs: 3 +echo $point2->x; // Outputs: 5 + + +A developer may define their own ''%%with%%'' method if they so choose. + +=== Performance considerations === + +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 are no longer needed. + + +$point1 = Point(3, 4); +$point2 = $point1; // No data duplication, $point2 references the same data as $point1 +$point3 = Point(3, 4); // No data duplication here either, it is pointing the the same memory as $point1 + +$point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance with modified data + + +== Cloning and with() == + +Calling ''%%clone%%'' on a ''%%record%%'' results in the exact 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%%''. + +''%%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)%%''. + +=== Equality === + +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%%''. + +Comparison operations will behave exactly like they do for classes. + +==== Reflection ==== + +Records in PHP will be fully supported by the reflection API, providing access to their properties and methods just like regular classes. However, immutability and special instantiation rules will be enforced. + +=== ReflectionClass support === + +''%%ReflectionClass%%'' can be used to inspect records, their properties, and methods. Any attempt to modify record properties via reflection will throw an exception, maintaining immutability. Attempting to create a new instance via ''%%ReflectionClass%%'' will cause a ''%%ReflectionException%%'' to be thrown. + + +$point = Point(3, 4); +$reflection = new \ReflectionClass($point); + +foreach ($reflection->getProperties() as $property) { + echo $property->getName() . ': ' . $property->getValue($point) . PHP_EOL; +} + + +=== Immutability enforcement === + +Attempts to modify record properties via reflection will throw an exception. + + +try { + $property = $reflection->getProperty('x'); + $property->setValue($point, 10); // This will throw an exception +} catch (\ReflectionException $e) { + echo 'Exception: ' . $e->getMessage() . PHP_EOL; // "Cannot modify a record property" +} + + +=== ReflectionFunction for implicit constructor === + +Using ''%%ReflectionFunction%%'' on a record will reflect the implicit constructor. + + +$constructor = new \ReflectionFunction('Geometry\Point'); +echo 'Constructor Parameters: '; +foreach ($constructor->getParameters() as $param) { + echo $param->getName() . ' '; +} + + +=== New functions and methods === + + * Calling ''%%is_object($record)%%'' will return ''%%true%%''. + * A new function, ''%%is_record($record)%%'', will return ''%%true%%'' for records, and ''%%false%%'' otherwise + * Calling ''%%get_class($record)%%'' will return the record name + +=== var_dump === + +Calling ''%%var_dump%%'' will look much like it does for objects, but instead of ''%%object%%'' it will say ''%%record%%''. + + +record(Point)#1 (2) { + ["x"]=> + int(1) + ["y"]=> + int(2) +} + + +==== Considerations for implementations ==== + +A ''%%record%%'' cannot be named after an existing ''%%record%%'', ''%%class%%'' or ''%%function%%''. This is because defining a ''%%record%%'' creates both a ''%%class%%'' and a ''%%function%%'' with the same name. + +==== Auto loading ==== + +As invoking a record value by its name looks remarkably similar to calling a function, and PHP has no function autoloader, auto loading will not be supported in this implementation. If function auto loading were to be implemented in the future, an autoloader could locate the ''%%record%%'' and autoload it. The author of this RFC strongly encourages someone to put forward a function auto loading RFC if auto loading is desired for records. + +===== Backward Incompatible Changes ===== + +No backward incompatible changes. + +===== Proposed PHP Version(s) ===== + +PHP 8.5 + +===== RFC Impact ===== + +==== To SAPIs ==== + +N/A + +==== To Existing Extensions ==== + +N/A + +==== To Opcache ==== + +Unknown. + +==== New Constants ==== + +None + +==== php.ini Defaults ==== + +None + +===== Open Issues ===== + +Todo + +===== Unaffected PHP Functionality ===== + +None. + +===== Future Scope ===== + +===== Proposed Voting Choices ===== + +Include these so readers know where you are heading and can discuss the proposed voting options. + +===== Patches and Tests ===== + +TBD + +===== Implementation ===== + +After the project is implemented, this section should contain + + - the version(s) it was merged into + - a link to the git commit(s) + - a link to the PHP manual entry for the feature + - a link to the language specification section (if any) + +===== References ===== + +Links to external references, discussions or RFCs + +===== Rejected Features ===== + +Keep this updated with features that were discussed on the mail lists. From 3be33e9703395c26f0a2675802d333a117383fe7 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 15:20:01 +0200 Subject: [PATCH 02/46] change format to ptxt --- drafts/.gitkeep | 0 published/.gitkeep | 0 published/{records.txt => records.ptxt} | 0 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 drafts/.gitkeep delete mode 100644 published/.gitkeep rename published/{records.txt => records.ptxt} (100%) diff --git a/drafts/.gitkeep b/drafts/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/published/.gitkeep b/published/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/published/records.txt b/published/records.ptxt similarity index 100% rename from published/records.txt rename to published/records.ptxt From bd4cf960864b88e4d66e8808039956b7e0aaffc8 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 16:59:42 +0200 Subject: [PATCH 03/46] apply some off-line feedback --- drafts/records.md | 268 ++++++++++++++++++++++++----------------- published/records.ptxt | 136 +++++++++++++++------ 2 files changed, 262 insertions(+), 142 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 5be28df..63c7fb2 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -8,90 +8,120 @@ ## Introduction -In modern PHP development, the need for concise and immutable data -structures is increasingly recognized. Inspired by the concept of -"records", "data objects", or "structs" in other languages, this RFC -proposes the addition of `record` objects in PHP. These records will -provide a concise and immutable data structure, distinct from `readonly` -classes, enabling developers to define immutable objects with less -boilerplate code. +This RFC proposed the introduction of `record` objects, which are immutable classes +with [value semantics](https://en.wikipedia.org/wiki/Value_semantics). + +### Value objects + +Value objects are immutable objects that represent a value. They are used to store values with a different meaning than +their technical value. +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. + +Consider this example: + +```php +function updateUserRole(int $userId, Role $role): void { + // ... +} + +$user = getUser(/*...*/) +$uid = $user->id; +// ... +$uid = 5; // somehow accidentally sets uid to an unrelated integer +// ... +updateUserRole($uid, Role::ADMIN()); // accidental passing of +``` + +In this example, the uid is accidentally set to a plain integer, and updateUserRole is called with the wrong value. + +Currently, the only solution to this is to use a class, but this requires a lot of boilerplate code. + +#### The solution + +Like arrays, strings, and other values, `record` objects are strongly equal to each other if they contain the same +values. + +Let's take a look, using the previous example: + +```php +record UserId(int $id); + +function updateUserRole(UserId $userId, Role $role): void { + // ... +} + +$user = getUser(/*...*/) +$uid = $user->id; // $uid is a UserId object +// ... +$uid = 5; +// ... +updateUserRole($uid, Role::ADMIN()); // This will throw an error +``` ## Proposal -This RFC proposes the introduction of a new record keyword in PHP to -define immutable data objects. These objects will allow properties to be -initialized concisely and will provide built-in methods for common -operations such as modifying properties and equality checks using a -function-like instantiation syntax. Records can implement interfaces and -use traits but cannot extend other records or classes. +This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will +allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying +properties and equality checks using a function-like instantiation syntax. +Records can implement interfaces and use traits but cannot extend other records or classes; +composition is allowed, however. #### Syntax and semantics ##### Definition -A `record` is defined by the word "record", followed by the name of its -type, an open parenthesis containing zero or more typed parameters that -become public, immutable, properties. They may optionally implement an -interface using the `implements` keyword. A `record` body is optional. +A `record` is defined by the word "record", followed by the name of its type, an open parenthesis containing one or more +typed parameters that become public, immutable, properties. +They may optionally implement an interface using the `implements` keyword. +A `record` body is optional. -A `record` may NOT contain a constructor; instead of defining initial -values, property hooks should be used to produce computable values -on-demand. Defining a constructor emits a compilation error. +A `record` may contain a constructor with zero arguments to perform further initialization, if required. +If it does not have a constructor, an implicit, empty contstructor is provided. -A `record` body may contain property hooks, methods, and use traits. -Regular properties may also be defined, but they are immutable by -default and are no different than `const`. +A `record` body may contain property hooks, methods, and use traits (so long as they do not conflict with `record` +rules). +Regular properties may also be defined, but they are immutable by default and are no different from `const`. Static properties and methods are forbidden in a `record` (this includes `const`, a regular property may be used instead). Attempting to define static properties, methods, constants results in a compilation error. ``` php -namespace Geometry; - -interface Shape { - public function area(): float; -} - -trait Dimension { - public function dimensions(): array { - return [$this->width, $this->height]; +namespace Paint; + +record Pigment(int $red, int $yellow, int $blue) { + public function mix(Pigment $other, float $amount): Pigment { + return $this->with( + red: $this->red * (1 - $amount) + $other->red * $amount, + yellow: $this->yellow * (1 - $amount) + $other->yellow * $amount, + blue: $this->blue * (1 - $amount) + $other->blue * $amount + ); } } -record Vector2(int $x, int $y); - -record Rectangle(Vector2 $leftTop, Vector2 $rightBottom) implements Shape { - use Dimension; +record StockPaint(Pigment $color, float $volume); - public int $height = 1; // this will always and forever be "1", it is immutable. - - public int $width { get => $this->rightBottom->x - $this->topLeft->x; } - public int $height { get => $this->rightBottom->y - $this->topLeft->y; } - - public function area(): float { - return $this->width * $this->height; +record PaintBucket(StockPaint ...$constituents) { + public function mixIn(StockPaint $paint): PaintBucket { + return $this->with(...$this->constituents, $paint); + } + + public function color(): Pigment { + return array_reduce($this->constituents, fn($color, $paint) => $color->mix($paint->color, $paint->volume), Pigment(0, 0, 0)); } } ``` ##### Usage -A `record` may be used as a `readonly class`, as the behavior of it is -very similar with no key differences to assist in migration from -`readonly class`. - -``` php -$rect1 = Rectangle(Point(0, 0), Point(1, -1)); -$rect2 = $rect1->with(topLeft: Point(0, 1)); - -var_dump($rect2->dimensions()); -``` +A `record` may be used as a `readonly class`, +as the behavior of it is very similar with no key differences to assist in migration from `readonly class`. ##### Optional parameters and default values -A `record` can also be defined with optional parameters that are set if -left out during instantiation. +A `record` can also be defined with optional parameters that are set if left out during instantiation. ``` php record Rectangle(int $x, int $y = 10); @@ -100,14 +130,12 @@ var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 ##### Auto-generated `with` method -To enhance the usability of records, the RFC proposes automatically -generating a `with` method for each record. This method allows for -partial updates of properties, creating a new instance of the record -with the specified properties updated. +To enhance the usability of records, the RFC proposes automatically generating a `with` method for each record. +This method allows for partial updates of properties, creating a new instance of the record with the specified +properties updated. -The auto-generated `with` method accepts only named arguments defined in -the constructor. No other property names can be used, and it returns a -new record object with the given values. +The auto-generated `with` method accepts only named arguments defined in the constructor. +No other property names can be used, and it returns a new record object with the given values. ``` php $point1 = Point(3, 4); @@ -118,15 +146,51 @@ echo $point1->x; // Outputs: 3 echo $point2->x; // Outputs: 5 ``` -A developer may define their own `with` method if they so choose. +A developer may define their own `with` method if they so choose, +and reference the generated `with` method using `parent::with()`. +This allows a developer to define policies or constraints on how data is updated. + +``` php +record Planet(string $name, int $population) { + public function with(int $population) { + return parent::with(population: $population); + } +} +$pluto = Planet("Pluto", 0); +// we made it! +$pluto = $pluto->with(population: 1); +// and then we changed the name +$mickey = $pluto->with(name: "Mickey"); // no named argument for population error +``` + +##### Constructors + +Optionally, they may also define a constructor to provide validation or other initialization logic: + +```php +record User(string $name, string $email) { + public string $id; + + public function __construct() { + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + throw new InvalidArgumentException("Invalid email address"); + } + + $this->id = hash('sha256', $email); + $this->name = ucwords($name); + } +} +``` + +During construction, a `record` is fully mutable. +This allows the developer freedom to mutate properties as needed to ensure a canonical representation of an object. #### Performance considerations -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 -are no longer needed. +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 are no +longer needed. ``` php $point1 = Point(3, 4); @@ -138,37 +202,33 @@ $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new i ##### Cloning and with() -Calling `clone` on a `record` results in the exact 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`. +Calling `clone` on a `record` results in the exact 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`. -`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)`. +`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)`. #### Equality -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`. +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`. Comparison operations will behave exactly like they do for classes. ### Reflection -Records in PHP will be fully supported by the reflection API, providing -access to their properties and methods just like regular classes. +Records in PHP will be fully supported by the reflection API, +providing access to their properties and methods just like regular classes. However, immutability and special instantiation rules will be enforced. #### ReflectionClass support -`ReflectionClass` can be used to inspect records, their properties, and -methods. Any attempt to modify record properties via reflection will -throw an exception, maintaining immutability. Attempting to create a new -instance via `ReflectionClass` will cause a `ReflectionException` to be -thrown. +`ReflectionClass` can be used to inspect records, their properties, and methods. Any attempt to modify record properties +via reflection will throw an exception, maintaining immutability. Attempting to create a new instance via +`ReflectionClass` will cause a `ReflectionException` to be thrown. ``` php $point = Point(3, 4); @@ -181,8 +241,7 @@ foreach ($reflection->getProperties() as $property) { #### Immutability enforcement -Attempts to modify record properties via reflection will throw an -exception. +Attempts to modify record properties via reflection will throw an exception. ``` php try { @@ -195,8 +254,7 @@ try { #### ReflectionFunction for implicit constructor -Using `ReflectionFunction` on a record will reflect the implicit -constructor. +Using `ReflectionFunction` on a record will reflect the implicit constructor. ``` php $constructor = new \ReflectionFunction('Geometry\Point'); @@ -209,14 +267,12 @@ foreach ($constructor->getParameters() as $param) { #### New functions and methods - Calling `is_object($record)` will return `true`. -- A new function, `is_record($record)`, will return `true` for records, - and `false` otherwise +- A new function, `is_record($record)`, will return `true` for records, and `false` otherwise - Calling `get_class($record)` will return the record name #### var_dump -Calling `var_dump` will look much like it does for objects, but instead -of `object` it will say `record`. +Calling `var_dump` will look much like it does for objects, but instead of `object` it will say `record`. record(Point)#1 (2) { ["x"]=> @@ -227,19 +283,15 @@ of `object` it will say `record`. ### Considerations for implementations -A `record` cannot be named after an existing `record`, `class` or -`function`. This is because defining a `record` creates both a `class` -and a `function` with the same name. +A `record` cannot be named after an existing `record`, `class` or `function`. This is because defining a `record` +creates both a `class` and a `function` with the same name. ### Auto loading -As invoking a record value by its name looks remarkably similar to -calling a function, and PHP has no function autoloader, auto loading -will not be supported in this implementation. If function auto loading -were to be implemented in the future, an autoloader could locate the -`record` and autoload it. The author of this RFC strongly encourages -someone to put forward a function auto loading RFC if auto loading is -desired for records. +As invoking a record value by its name looks remarkably similar to calling a function, +and PHP has no function autoloader, auto loading will not be supported in this implementation. +If function auto loading were to be implemented in the future, an autoloader could locate the `record` and autoload it. +The author of this RFC strongly encourages someone to put forward a function auto loading RFC if auto loading is desired for records. ## Backward Incompatible Changes @@ -294,10 +346,10 @@ TBD After the project is implemented, this section should contain -1. the version(s) it was merged into -2. a link to the git commit(s) -3. a link to the PHP manual entry for the feature -4. a link to the language specification section (if any) +1. the version(s) it was merged into +2. a link to the git commit(s) +3. a link to the PHP manual entry for the feature +4. a link to the language specification section (if any) ## References diff --git a/published/records.ptxt b/published/records.ptxt index 956fa61..f3cd00f 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -8,49 +8,90 @@ ===== Introduction ===== -In modern PHP development, the need for concise and immutable data structures is increasingly recognized. Inspired by the concept of "records", "data objects", or "structs" in other languages, this RFC proposes the addition of ''%%record%%'' objects in PHP. These records will provide a concise and immutable data structure, distinct from ''%%readonly%%'' classes, enabling developers to define immutable objects with less boilerplate code. +This RFC proposed the introduction of ''%%record%%'' objects, which are immutable classes with [[https://en.wikipedia.org/wiki/Value_semantics|value semantics]]. -===== Proposal ===== +==== Value objects ==== -This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but cannot extend other records or classes. +Value objects are immutable objects that represent a value. They are used to store values with a different meaning than their technical value. 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. -=== Syntax and semantics === +Consider this example: -== Definition == + +function updateUserRole(int $userId, Role $role): void { + // ... +} -A ''%%record%%'' is defined by the word "record", followed by the name of its type, an open parenthesis containing zero or more typed parameters that become public, immutable, properties. They may optionally implement an interface using the ''%%implements%%'' keyword. A ''%%record%%'' body is optional. +$user = getUser(/*...*/) +$uid = $user->id; +// ... +$uid = 5; // somehow accidentally sets uid to an unrelated integer +// ... +updateUserRole($uid, Role::ADMIN()); // accidental passing of + -A ''%%record%%'' may NOT contain a constructor; instead of defining initial values, property hooks should be used to produce computable values on-demand. Defining a constructor emits a compilation error. +In this example, the uid is accidentally set to a plain integer, and updateUserRole is called with the wrong value. -A ''%%record%%'' body may contain property hooks, methods, and use traits. Regular properties may also be defined, but they are immutable by default and are no different than ''%%const%%''. +Currently, the only solution to this is to use a class, but this requires a lot of boilerplate code. -Static properties and methods are forbidden in a ''%%record%%'' (this includes ''%%const%%'', a regular property may be used instead). Attempting to define static properties, methods, constants results in a compilation error. +=== The solution === + +Like arrays, strings, and other values, ''%%record%%'' objects are strongly equal to each other if they contain the same values. + +Let's take a look, using the previous example: -namespace Geometry; +record UserId(int $id); -interface Shape { - public function area(): float; +function updateUserRole(UserId $userId, Role $role): void { + // ... } -trait Dimension { - public function dimensions(): array { - return [$this->width, $this->height]; - } -} +$user = getUser(/*...*/) +$uid = $user->id; // $uid is a UserId object +// ... +$uid = 5; +// ... +updateUserRole($uid, Role::ADMIN()); // This will throw an error + + +===== Proposal ===== -record Vector2(int $x, int $y); +This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but cannot extend other records or classes; composition is allowed, however. -record Rectangle(Vector2 $leftTop, Vector2 $rightBottom) implements Shape { - use Dimension; +=== Syntax and semantics === + +== Definition == - public int $height = 1; // this will always and forever be "1", it is immutable. +A ''%%record%%'' is defined by the word "record", followed by the name of its type, an open parenthesis containing one or more typed parameters that become public, immutable, properties. They may optionally implement an interface using the ''%%implements%%'' keyword. A ''%%record%%'' body is optional. - public int $width { get => $this->rightBottom->x - $this->topLeft->x; } - public int $height { get => $this->rightBottom->y - $this->topLeft->y; } +A ''%%record%%'' may contain a constructor with zero arguments to perform further initialization, if required. If it does not have a constructor, an implicit, empty contstructor is provided. - public function area(): float { - return $this->width * $this->height; +A ''%%record%%'' body may contain property hooks, methods, and use traits (so long as they do not conflict with ''%%record%%'' rules). Regular properties may also be defined, but they are immutable by default and are no different from ''%%const%%''. + +Static properties and methods are forbidden in a ''%%record%%'' (this includes ''%%const%%'', a regular property may be used instead). Attempting to define static properties, methods, constants results in a compilation error. + + +namespace Paint; + +record Pigment(int $red, int $yellow, int $blue) { + public function mix(Pigment $other, float $amount): Pigment { + return $this->with( + red: $this->red * (1 - $amount) + $other->red * $amount, + yellow: $this->yellow * (1 - $amount) + $other->yellow * $amount, + blue: $this->blue * (1 - $amount) + $other->blue * $amount + ); + } +} + +record StockPaint(Pigment $color, float $volume); + +record PaintBucket(StockPaint ...$constituents) { + public function mixIn(StockPaint $paint): PaintBucket { + return $this->with(...$this->constituents, $paint); + } + + public function color(): Pigment { + return array_reduce($this->constituents, fn($color, $paint) => $color->mix($paint->color, $paint->volume), Pigment(0, 0, 0)); } } @@ -59,13 +100,6 @@ record Rectangle(Vector2 $leftTop, Vector2 $rightBottom) implements Shape { A ''%%record%%'' may be used as a ''%%readonly class%%'', as the behavior of it is very similar with no key differences to assist in migration from ''%%readonly class%%''. - -$rect1 = Rectangle(Point(0, 0), Point(1, -1)); -$rect2 = $rect1->with(topLeft: Point(0, 1)); - -var_dump($rect2->dimensions()); - - == Optional parameters and default values == A ''%%record%%'' can also be defined with optional parameters that are set if left out during instantiation. @@ -90,7 +124,41 @@ echo $point1->x; // Outputs: 3 echo $point2->x; // Outputs: 5 -A developer may define their own ''%%with%%'' method if they so choose. +A developer may define their own ''%%with%%'' method if they so choose, and reference the generated ''%%with%%'' method using ''%%parent::with()%%''. This allows a developer to define policies or constraints on how data is updated. + + +record Planet(string $name, int $population) { + public function with(int $population) { + return parent::with(population: $population); + } +} +$pluto = Planet("Pluto", 0); +// we made it! +$pluto = $pluto->with(population: 1); +// and then we changed the name +$mickey = $pluto->with(name: "Mickey"); // no named argument for population error + + +== Constructors == + +Optionally, they may also define a constructor to provide validation or other initialization logic: + + +record User(string $name, string $email) { + public string $id; + + public function __construct() { + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + throw new InvalidArgumentException("Invalid email address"); + } + + $this->id = hash('sha256', $email); + $this->name = ucwords($name); + } +} + + +During construction, a ''%%record%%'' is fully mutable. This allows the developer freedom to mutate properties as needed to ensure a canonical representation of an object. === Performance considerations === @@ -108,7 +176,7 @@ $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new i Calling ''%%clone%%'' on a ''%%record%%'' results in the exact 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%%''. -''%%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)%%''. +''%%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)%%''. === Equality === From c69bff394884dd3c39b75ee0eb9ba6b2d932d0d5 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 17:00:40 +0200 Subject: [PATCH 04/46] apply some off-line feedback --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 33420e8..fac4dff 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ all: $(PUBLISHED) .git/hooks/pre-commit drafts/template.md: template.ptxt @echo "Creating draft from template" - src/convert-to-md.sh template.txt drafts/template.md + src/convert-to-md.sh template.ptxt drafts/template.md published/%.ptxt: drafts/%.md @echo "Converting $< to $@" From 49be8823dedb96678e7121da78f0db4d9b227d81 Mon Sep 17 00:00:00 2001 From: Rob Landers Date: Thu, 1 Aug 2024 17:07:25 +0200 Subject: [PATCH 05/46] Update drafts/records.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Rob Landers --- drafts/records.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drafts/records.md b/drafts/records.md index 63c7fb2..07227e5 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -13,7 +13,7 @@ with [value semantics](https://en.wikipedia.org/wiki/Value_semantics). ### Value objects -Value objects are immutable objects that represent a value. They are used to store values with a different meaning than +Value objects are immutable objects that represent a value. They are used for storing values with a different meaning than their technical value. 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. From 090855f06d799c0ded24adf5ed75855d93a12c5e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 1 Aug 2024 15:07:51 +0000 Subject: [PATCH 06/46] Automated changes from GitHub Actions signed: Rob Landers --- published/records.ptxt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/published/records.ptxt b/published/records.ptxt index f3cd00f..33e170e 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -12,7 +12,7 @@ This RFC proposed the introduction of ''%%record%%'' objects, which are immutabl ==== Value objects ==== -Value objects are immutable objects that represent a value. They are used to store values with a different meaning than their technical value. 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. +Value objects are immutable objects that represent a value. They are used for storing values with a different meaning than their technical value. 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. Consider this example: From c91e18c05654c6dfca67638cdd8d9d37401fb381 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 17:02:18 +0200 Subject: [PATCH 07/46] be more assertive --- .coderabbit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coderabbit.yml b/.coderabbit.yml index 20120d7..ab2a3ad 100644 --- a/.coderabbit.yml +++ b/.coderabbit.yml @@ -3,7 +3,7 @@ tone_instructions: '' early_access: false enable_free_tier: true reviews: - profile: chill + profile: assertive request_changes_workflow: false high_level_summary: true high_level_summary_placeholder: '@coderabbitai summary' From 7b1e32b92000f7304d5a4455cc6d36a433b504e7 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 17:04:29 +0200 Subject: [PATCH 08/46] fix lint --- drafts/records.md | 34 ++++++++++++++++++---------------- published/records.ptxt | 28 ++++++++++++++-------------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 07227e5..8bc77bf 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -68,9 +68,9 @@ properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but cannot extend other records or classes; composition is allowed, however. -#### Syntax and semantics +### Syntax and semantics -##### Definition +#### Definition A `record` is defined by the word "record", followed by the name of its type, an open parenthesis containing one or more typed parameters that become public, immutable, properties. @@ -114,12 +114,12 @@ record PaintBucket(StockPaint ...$constituents) { } ``` -##### Usage +#### Usage A `record` may be used as a `readonly class`, as the behavior of it is very similar with no key differences to assist in migration from `readonly class`. -##### Optional parameters and default values +#### Optional parameters and default values A `record` can also be defined with optional parameters that are set if left out during instantiation. @@ -128,7 +128,7 @@ record Rectangle(int $x, int $y = 10); var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 ``` -##### Auto-generated `with` method +#### Auto-generated `with` method To enhance the usability of records, the RFC proposes automatically generating a `with` method for each record. This method allows for partial updates of properties, creating a new instance of the record with the specified @@ -163,7 +163,7 @@ $pluto = $pluto->with(population: 1); $mickey = $pluto->with(name: "Mickey"); // no named argument for population error ``` -##### Constructors +#### Constructors Optionally, they may also define a constructor to provide validation or other initialization logic: @@ -185,7 +185,7 @@ record User(string $name, string $email) { During construction, a `record` is fully mutable. This allows the developer freedom to mutate properties as needed to ensure a canonical representation of an object. -#### Performance considerations +### Performance considerations 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. @@ -200,7 +200,7 @@ $point3 = Point(3, 4); // No data duplication here either, it is pointing the th $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance with modified data ``` -##### Cloning and with() +#### Cloning and with() Calling `clone` on a `record` results in the exact 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`. @@ -210,7 +210,7 @@ This is an important consideration because a developer may call `$new = $record- crash. If a developer wants to crash, they can do by `assert($new !== $record)`. -#### Equality +### Equality 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. @@ -270,16 +270,18 @@ foreach ($constructor->getParameters() as $param) { - A new function, `is_record($record)`, will return `true` for records, and `false` otherwise - Calling `get_class($record)` will return the record name -#### var_dump +### var_dump Calling `var_dump` will look much like it does for objects, but instead of `object` it will say `record`. - record(Point)#1 (2) { - ["x"]=> - int(1) - ["y"]=> - int(2) - } +```txt +record(Point)#1 (2) { + ["x"]=> + int(1) + ["y"]=> + int(2) +} +``` ### Considerations for implementations diff --git a/published/records.ptxt b/published/records.ptxt index 33e170e..fe5bc25 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -26,7 +26,7 @@ $uid = $user->id; // ... $uid = 5; // somehow accidentally sets uid to an unrelated integer // ... -updateUserRole($uid, Role::ADMIN()); // accidental passing of +updateUserRole($uid, Role::ADMIN()); // accidental passing of In this example, the uid is accidentally set to a plain integer, and updateUserRole is called with the wrong value. @@ -58,9 +58,9 @@ updateUserRole($uid, Role::ADMIN()); // This will throw an error This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but cannot extend other records or classes; composition is allowed, however. -=== Syntax and semantics === +==== Syntax and semantics ==== -== Definition == +=== Definition === A ''%%record%%'' is defined by the word "record", followed by the name of its type, an open parenthesis containing one or more typed parameters that become public, immutable, properties. They may optionally implement an interface using the ''%%implements%%'' keyword. A ''%%record%%'' body is optional. @@ -89,18 +89,18 @@ record PaintBucket(StockPaint ...$constituents) { public function mixIn(StockPaint $paint): PaintBucket { return $this->with(...$this->constituents, $paint); } - + public function color(): Pigment { return array_reduce($this->constituents, fn($color, $paint) => $color->mix($paint->color, $paint->volume), Pigment(0, 0, 0)); } } -== Usage == +=== Usage === A ''%%record%%'' may be used as a ''%%readonly class%%'', as the behavior of it is very similar with no key differences to assist in migration from ''%%readonly class%%''. -== Optional parameters and default values == +=== Optional parameters and default values === A ''%%record%%'' can also be defined with optional parameters that are set if left out during instantiation. @@ -109,7 +109,7 @@ record Rectangle(int $x, int $y = 10); var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 -== Auto-generated with method == +=== Auto-generated with method === To enhance the usability of records, the RFC proposes automatically generating a ''%%with%%'' method for each record. This method allows for partial updates of properties, creating a new instance of the record with the specified properties updated. @@ -139,7 +139,7 @@ $pluto = $pluto->with(population: 1); $mickey = $pluto->with(name: "Mickey"); // no named argument for population error -== Constructors == +=== Constructors === Optionally, they may also define a constructor to provide validation or other initialization logic: @@ -151,7 +151,7 @@ record User(string $name, string $email) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException("Invalid email address"); } - + $this->id = hash('sha256', $email); $this->name = ucwords($name); } @@ -160,7 +160,7 @@ record User(string $name, string $email) { During construction, a ''%%record%%'' is fully mutable. This allows the developer freedom to mutate properties as needed to ensure a canonical representation of an object. -=== Performance considerations === +==== Performance considerations ==== 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 are no longer needed. @@ -172,13 +172,13 @@ $point3 = Point(3, 4); // No data duplication here either, it is pointing the th $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance with modified data -== Cloning and with() == +=== Cloning and with() === Calling ''%%clone%%'' on a ''%%record%%'' results in the exact 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%%''. ''%%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)%%''. -=== Equality === +==== Equality ==== 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%%''. @@ -232,11 +232,11 @@ foreach ($constructor->getParameters() as $param) { * A new function, ''%%is_record($record)%%'', will return ''%%true%%'' for records, and ''%%false%%'' otherwise * Calling ''%%get_class($record)%%'' will return the record name -=== var_dump === +==== var_dump ==== Calling ''%%var_dump%%'' will look much like it does for objects, but instead of ''%%object%%'' it will say ''%%record%%''. - + record(Point)#1 (2) { ["x"]=> int(1) From be7ae3159d0a752e650a57d73aeec828e5db50d6 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 17:09:13 +0200 Subject: [PATCH 09/46] address automated feedback --- drafts/records.md | 14 +++++++------- published/records.ptxt | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 8bc77bf..4ce54df 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -77,7 +77,7 @@ typed parameters that become public, immutable, properties. They may optionally implement an interface using the `implements` keyword. A `record` body is optional. -A `record` may contain a constructor with zero arguments to perform further initialization, if required. +A `record` may contain a constructor with zero arguments to perform further initialization if required. If it does not have a constructor, an implicit, empty contstructor is provided. A `record` body may contain property hooks, methods, and use traits (so long as they do not conflict with `record` @@ -226,7 +226,7 @@ However, immutability and special instantiation rules will be enforced. #### ReflectionClass support -`ReflectionClass` can be used to inspect records, their properties, and methods. Any attempt to modify record properties +It can be used to inspect records, their properties, and methods. Any attempt to modify record properties via reflection will throw an exception, maintaining immutability. Attempting to create a new instance via `ReflectionClass` will cause a `ReflectionException` to be thrown. @@ -288,12 +288,12 @@ record(Point)#1 (2) { A `record` cannot be named after an existing `record`, `class` or `function`. This is because defining a `record` creates both a `class` and a `function` with the same name. -### Auto loading +### Autoloading As invoking a record value by its name looks remarkably similar to calling a function, -and PHP has no function autoloader, auto loading will not be supported in this implementation. -If function auto loading were to be implemented in the future, an autoloader could locate the `record` and autoload it. -The author of this RFC strongly encourages someone to put forward a function auto loading RFC if auto loading is desired for records. +and PHP has no function autoloader, autoloading will not be supported in this implementation. +If function autoloading were to be implemented in the future, an autoloader could locate the `record` and autoload it. +The author of this RFC strongly encourages someone to put forward a function autoloading RFC if autoloading is desired for records. ## Backward Incompatible Changes @@ -327,7 +327,7 @@ None ## Open Issues -Todo +To-do ## Unaffected PHP Functionality diff --git a/published/records.ptxt b/published/records.ptxt index fe5bc25..c2089da 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -26,7 +26,7 @@ $uid = $user->id; // ... $uid = 5; // somehow accidentally sets uid to an unrelated integer // ... -updateUserRole($uid, Role::ADMIN()); // accidental passing of +updateUserRole($uid, Role::ADMIN()); // accidental passing of In this example, the uid is accidentally set to a plain integer, and updateUserRole is called with the wrong value. @@ -64,7 +64,7 @@ This RFC proposes the introduction of a new record keyword in PHP to define immu A ''%%record%%'' is defined by the word "record", followed by the name of its type, an open parenthesis containing one or more typed parameters that become public, immutable, properties. They may optionally implement an interface using the ''%%implements%%'' keyword. A ''%%record%%'' body is optional. -A ''%%record%%'' may contain a constructor with zero arguments to perform further initialization, if required. If it does not have a constructor, an implicit, empty contstructor is provided. +A ''%%record%%'' may contain a constructor with zero arguments to perform further initialization if required. If it does not have a constructor, an implicit, empty contstructor is provided. A ''%%record%%'' body may contain property hooks, methods, and use traits (so long as they do not conflict with ''%%record%%'' rules). Regular properties may also be defined, but they are immutable by default and are no different from ''%%const%%''. @@ -89,7 +89,7 @@ record PaintBucket(StockPaint ...$constituents) { public function mixIn(StockPaint $paint): PaintBucket { return $this->with(...$this->constituents, $paint); } - + public function color(): Pigment { return array_reduce($this->constituents, fn($color, $paint) => $color->mix($paint->color, $paint->volume), Pigment(0, 0, 0)); } @@ -151,7 +151,7 @@ record User(string $name, string $email) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException("Invalid email address"); } - + $this->id = hash('sha256', $email); $this->name = ucwords($name); } @@ -190,7 +190,7 @@ Records in PHP will be fully supported by the reflection API, providing access t === ReflectionClass support === -''%%ReflectionClass%%'' can be used to inspect records, their properties, and methods. Any attempt to modify record properties via reflection will throw an exception, maintaining immutability. Attempting to create a new instance via ''%%ReflectionClass%%'' will cause a ''%%ReflectionException%%'' to be thrown. +It can be used to inspect records, their properties, and methods. Any attempt to modify record properties via reflection will throw an exception, maintaining immutability. Attempting to create a new instance via ''%%ReflectionClass%%'' will cause a ''%%ReflectionException%%'' to be thrown. $point = Point(3, 4); @@ -249,9 +249,9 @@ record(Point)#1 (2) { A ''%%record%%'' cannot be named after an existing ''%%record%%'', ''%%class%%'' or ''%%function%%''. This is because defining a ''%%record%%'' creates both a ''%%class%%'' and a ''%%function%%'' with the same name. -==== Auto loading ==== +==== Autoloading ==== -As invoking a record value by its name looks remarkably similar to calling a function, and PHP has no function autoloader, auto loading will not be supported in this implementation. If function auto loading were to be implemented in the future, an autoloader could locate the ''%%record%%'' and autoload it. The author of this RFC strongly encourages someone to put forward a function auto loading RFC if auto loading is desired for records. +As invoking a record value by its name looks remarkably similar to calling a function, and PHP has no function autoloader, autoloading will not be supported in this implementation. If function autoloading were to be implemented in the future, an autoloader could locate the ''%%record%%'' and autoload it. The author of this RFC strongly encourages someone to put forward a function autoloading RFC if autoloading is desired for records. ===== Backward Incompatible Changes ===== @@ -285,7 +285,7 @@ None ===== Open Issues ===== -Todo +To-do ===== Unaffected PHP Functionality ===== From e9a1cc1c2d724fced3925b2c378539a1ca8a0924 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 17:33:52 +0200 Subject: [PATCH 10/46] clarify --- drafts/records.md | 5 ++++- published/records.ptxt | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 4ce54df..e401493 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -14,7 +14,7 @@ with [value semantics](https://en.wikipedia.org/wiki/Value_semantics). ### Value objects Value objects are immutable objects that represent a value. They are used for storing values with a different meaning than -their technical value. +their technical value, adding additional semantic context to the value. 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. @@ -60,6 +60,9 @@ $uid = 5; updateUserRole($uid, Role::ADMIN()); // This will throw an error ``` +Now, if `$uid` is accidentally set to an integer, +the call to `updateUserRole` will throw an error because the type is not correct. + ## Proposal This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will diff --git a/published/records.ptxt b/published/records.ptxt index c2089da..e494684 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -12,7 +12,7 @@ This RFC proposed the introduction of ''%%record%%'' objects, which are immutabl ==== Value objects ==== -Value objects are immutable objects that represent a value. They are used for storing values with a different meaning than their technical value. 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. +Value objects are immutable objects that represent a value. They are used for storing values with a different meaning than their technical value, adding additional semantic context to the value. 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. Consider this example: @@ -54,6 +54,8 @@ $uid = 5; updateUserRole($uid, Role::ADMIN()); // This will throw an error +Now, if ''%%$uid%%'' is accidentally set to an integer, the call to ''%%updateUserRole%%'' will throw an error because the type is not correct. + ===== Proposal ===== This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but cannot extend other records or classes; composition is allowed, however. @@ -178,6 +180,21 @@ Calling ''%%clone%%'' on a ''%%record%%'' results in the exact same record objec ''%%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)%%''. +==== Serialization and deserialization ==== + +Records that contain a single value will serialize to their single value, while records with multiple values will serialize to an array of values. The same will be true for deserialization. + + +record Single(string $value); +record Multiple(string $value1, string $value2); + +echo $single = serialize(Single('value')); // Outputs: "s:5:"value";" +echo $multiple = serialize(Multiple('value1', 'value2')); // Outputs: "a:2:{i:0;s:6:"value1";i:1;s:6:"value2";}" + +echo unserialize($single) === Single('value'); // Outputs: true +echo unserialize($multiple) === Multiple('value1', 'value2'); // Outputs: true + + ==== Equality ==== 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%%''. From 97057b43f23298ed441a5b66aa44dfaf636a6c4f Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 17:34:07 +0200 Subject: [PATCH 11/46] serialization and deserialization --- drafts/records.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/drafts/records.md b/drafts/records.md index e401493..7df854c 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -213,6 +213,23 @@ This is an important consideration because a developer may call `$new = $record- crash. If a developer wants to crash, they can do by `assert($new !== $record)`. +### Serialization and deserialization + +Records that contain a single value will serialize to their single value, +while records with multiple values will serialize to an array of values. +The same will be true for deserialization. + +```php +record Single(string $value); +record Multiple(string $value1, string $value2); + +echo $single = serialize(Single('value')); // Outputs: "s:5:"value";" +echo $multiple = serialize(Multiple('value1', 'value2')); // Outputs: "a:2:{i:0;s:6:"value1";i:1;s:6:"value2";}" + +echo unserialize($single) === Single('value'); // Outputs: true +echo unserialize($multiple) === Multiple('value1', 'value2'); // Outputs: true +``` + ### Equality A `record` is always strongly equal (`===`) to another record with the same value in the properties, From fc83031a67effa37fb173c2023fcea670fcf3c8d Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 18:00:51 +0200 Subject: [PATCH 12/46] clarity --- drafts/records.md | 22 ++++++++++++++++++++-- published/records.ptxt | 22 ++++++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 7df854c..0fb2cf6 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -236,7 +236,25 @@ A `record` is always strongly equal (`===`) to another record with the same valu 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`. -Comparison operations will behave exactly like they do for classes. +Comparison operations will behave exactly like they do for classes, for example: + +```php +record Time(float $milliseconds = 0) { + public float $totalSeconds { + get => $this->milliseconds / 1000, + } + + public float $totalMinutes { + get => $this->totalSeconds / 60, + } + /* ... */ +} + +$time1 = Time(1000); +$time2 = Time(5000); + +echo $time1 < $time2; // Outputs: true +``` ### Reflection @@ -274,7 +292,7 @@ try { #### ReflectionFunction for implicit constructor -Using `ReflectionFunction` on a record will reflect the implicit constructor. +Using `ReflectionFunction` on a record will reflect the constructor. ``` php $constructor = new \ReflectionFunction('Geometry\Point'); diff --git a/published/records.ptxt b/published/records.ptxt index e494684..2f9cca6 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -199,7 +199,25 @@ echo unserialize($multiple) === Multiple('value1', 'value2'); // Outputs: true 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%%''. -Comparison operations will behave exactly like they do for classes. +Comparison operations will behave exactly like they do for classes, for example: + + +record Time(float $milliseconds = 0) { + public float $totalSeconds { + get => $this->milliseconds / 1000, + } + + public float $totalMinutes { + get => $this->totalSeconds / 60, + } + /* ... */ +} + +$time1 = Time(1000); +$time2 = Time(5000); + +echo $time1 < $time2; // Outputs: true + ==== Reflection ==== @@ -233,7 +251,7 @@ try { === ReflectionFunction for implicit constructor === -Using ''%%ReflectionFunction%%'' on a record will reflect the implicit constructor. +Using ''%%ReflectionFunction%%'' on a record will reflect the constructor. $constructor = new \ReflectionFunction('Geometry\Point'); From 62c8edd9950f822393770904b3e8082f091f34f7 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 18:02:51 +0200 Subject: [PATCH 13/46] fix serialization and deserialization --- drafts/records.md | 8 +++----- published/records.ptxt | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 0fb2cf6..cac2f8b 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -215,16 +215,14 @@ If a developer wants to crash, they can do by `assert($new !== $record)`. ### Serialization and deserialization -Records that contain a single value will serialize to their single value, -while records with multiple values will serialize to an array of values. -The same will be true for deserialization. +Records are fully serializable and deserializable. ```php record Single(string $value); record Multiple(string $value1, string $value2); -echo $single = serialize(Single('value')); // Outputs: "s:5:"value";" -echo $multiple = serialize(Multiple('value1', 'value2')); // Outputs: "a:2:{i:0;s:6:"value1";i:1;s:6:"value2";}" +echo $single = serialize(Single('value')); // Outputs: "O:6:"Single":1:{s:5:"value";s:5:"value";}" +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";}}" echo unserialize($single) === Single('value'); // Outputs: true echo unserialize($multiple) === Multiple('value1', 'value2'); // Outputs: true diff --git a/published/records.ptxt b/published/records.ptxt index 2f9cca6..48d8354 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -182,14 +182,14 @@ Calling ''%%clone%%'' on a ''%%record%%'' results in the exact same record objec ==== Serialization and deserialization ==== -Records that contain a single value will serialize to their single value, while records with multiple values will serialize to an array of values. The same will be true for deserialization. +Records are fully serializable and deserializable. record Single(string $value); record Multiple(string $value1, string $value2); -echo $single = serialize(Single('value')); // Outputs: "s:5:"value";" -echo $multiple = serialize(Multiple('value1', 'value2')); // Outputs: "a:2:{i:0;s:6:"value1";i:1;s:6:"value2";}" +echo $single = serialize(Single('value')); // Outputs: "O:6:"Single":1:{s:5:"value";s:5:"value";}" +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";}}" echo unserialize($single) === Single('value'); // Outputs: true echo unserialize($multiple) === Multiple('value1', 'value2'); // Outputs: true From a490ba09dc483a60ace83f4ca4eb7c9337d62b0c Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 18:06:14 +0200 Subject: [PATCH 14/46] apply some feedback --- drafts/records.md | 8 ++++---- published/records.ptxt | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index cac2f8b..844850f 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -8,7 +8,7 @@ ## Introduction -This RFC proposed the introduction of `record` objects, which are immutable classes +This RFC proposes the introduction of `record` objects, which are immutable classes with [value semantics](https://en.wikipedia.org/wiki/Value_semantics). ### Value objects @@ -124,7 +124,7 @@ as the behavior of it is very similar with no key differences to assist in migra #### Optional parameters and default values -A `record` can also be defined with optional parameters that are set if left out during instantiation. +A `record` can also be defined with optional parameters that are set if omitted during instantiation. ``` php record Rectangle(int $x, int $y = 10); @@ -138,7 +138,7 @@ This method allows for partial updates of properties, creating a new instance of properties updated. The auto-generated `with` method accepts only named arguments defined in the constructor. -No other property names can be used, and it returns a new record object with the given values. +No other property names can be used, and it returns a record object with the given values. ``` php $point1 = Point(3, 4); @@ -205,7 +205,7 @@ $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new i #### Cloning and with() -Calling `clone` on a `record` results in the exact same record object being returned. As it is a "value" object, it +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`. `with` may be called with no arguments, and it is the same behavior as `clone`. diff --git a/published/records.ptxt b/published/records.ptxt index 48d8354..e73b1ae 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -8,7 +8,7 @@ ===== Introduction ===== -This RFC proposed the introduction of ''%%record%%'' objects, which are immutable classes with [[https://en.wikipedia.org/wiki/Value_semantics|value semantics]]. +This RFC proposes the introduction of ''%%record%%'' objects, which are immutable classes with [[https://en.wikipedia.org/wiki/Value_semantics|value semantics]]. ==== Value objects ==== @@ -104,7 +104,7 @@ A ''%%record%%'' may be used as a ''%%readonly class%%'', as the behavior of it === Optional parameters and default values === -A ''%%record%%'' can also be defined with optional parameters that are set if left out during instantiation. +A ''%%record%%'' can also be defined with optional parameters that are set if omitted during instantiation. record Rectangle(int $x, int $y = 10); @@ -115,7 +115,7 @@ var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 To enhance the usability of records, the RFC proposes automatically generating a ''%%with%%'' method for each record. This method allows for partial updates of properties, creating a new instance of the record with the specified properties updated. -The auto-generated ''%%with%%'' method accepts only named arguments defined in the constructor. No other property names can be used, and it returns a new record object with the given values. +The auto-generated ''%%with%%'' method accepts only named arguments defined in the constructor. No other property names can be used, and it returns a record object with the given values. $point1 = Point(3, 4); @@ -176,7 +176,7 @@ $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new i === Cloning and with() === -Calling ''%%clone%%'' on a ''%%record%%'' results in the exact 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%%''. +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%%''. ''%%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)%%''. From eb4f020c5c47d3a001d9d816a5e57b6e0624bd33 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 18:07:35 +0200 Subject: [PATCH 15/46] experiment with instructions --- .coderabbit.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.coderabbit.yml b/.coderabbit.yml index ab2a3ad..ba37b46 100644 --- a/.coderabbit.yml +++ b/.coderabbit.yml @@ -18,7 +18,10 @@ reviews: path_instructions: [ { "path": "drafts/*.md", - "instructions": "These are PHP RFC's to change the PHP language. Be constructive but critical in how it may change the language" + "instructions": | + These are PHP RFC's to change the PHP language. Do not just consider the grammar of the text, but consider how + it might change the language. For example, if a new feature is being added, consider how it might be used, and + propose better ideas if you have them. } ] abort_on_close: true From 9089f440c4aea2ab15176acce2da917ad6d5fb31 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 18:13:07 +0200 Subject: [PATCH 16/46] make parsable instructions since yaml parser is broke --- .coderabbit.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.coderabbit.yml b/.coderabbit.yml index ba37b46..d21bb16 100644 --- a/.coderabbit.yml +++ b/.coderabbit.yml @@ -18,10 +18,7 @@ reviews: path_instructions: [ { "path": "drafts/*.md", - "instructions": | - These are PHP RFC's to change the PHP language. Do not just consider the grammar of the text, but consider how - it might change the language. For example, if a new feature is being added, consider how it might be used, and - propose better ideas if you have them. + "instructions": "These are PHP RFC's to change the PHP language. Do not just consider the grammar of the text, but consider how it might change the language. For example, if a new feature is being added, consider how it might be used, and propose better ideas if you have them." } ] abort_on_close: true From 6b32127390e8dd8c14aee2f049447d84077f3e66 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 1 Aug 2024 18:47:56 +0200 Subject: [PATCH 17/46] add more details to example --- drafts/records.md | 27 ++++++++++++++++++++++++--- published/records.ptxt | 27 ++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 844850f..7bd83ec 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -94,7 +94,15 @@ static properties, methods, constants results in a compilation error. ``` php namespace Paint; +// Define a record with several primary color properties record Pigment(int $red, int $yellow, int $blue) { + + // property hooks are allowed + public string $hexValue { + get => sprintf("#%02x%02x%02x", $this->red, $this->yellow, $this->blue), + } + + // methods are allowed public function mix(Pigment $other, float $amount): Pigment { return $this->with( red: $this->red * (1 - $amount) + $other->red * $amount, @@ -102,15 +110,28 @@ record Pigment(int $red, int $yellow, int $blue) { blue: $this->blue * (1 - $amount) + $other->blue * $amount ); } + + // all properties are mutable in constructors + public function __construct() { + $this->red = max(0, min(255, $this->red)); + $this->yellow = max(0, min(255, $this->yellow)); + $this->blue = max(0, min(255, $this->blue)); + } + + public function with() { + // prevent the creation of a new Pigment from an existing pigment + throw new \LogicException("Cannot create a new Pigment from an existing pigment"); + } } +// simple records do not need to define a body record StockPaint(Pigment $color, float $volume); record PaintBucket(StockPaint ...$constituents) { public function mixIn(StockPaint $paint): PaintBucket { - return $this->with(...$this->constituents, $paint); + return $this->with(...[...$this->constituents, $paint]); } - + public function color(): Pigment { return array_reduce($this->constituents, fn($color, $paint) => $color->mix($paint->color, $paint->volume), Pigment(0, 0, 0)); } @@ -119,7 +140,7 @@ record PaintBucket(StockPaint ...$constituents) { #### Usage -A `record` may be used as a `readonly class`, +A `record` may be used any where a `readonly class` can be used, as the behavior of it is very similar with no key differences to assist in migration from `readonly class`. #### Optional parameters and default values diff --git a/published/records.ptxt b/published/records.ptxt index e73b1ae..8263306 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -75,7 +75,15 @@ Static properties and methods are forbidden in a ''%%record%%'' (this includes ' namespace Paint; +// Define a record with several primary color properties record Pigment(int $red, int $yellow, int $blue) { + + // property hooks are allowed + public string $hexValue { + get => sprintf("#%02x%02x%02x", $this->red, $this->yellow, $this->blue), + } + + // methods are allowed public function mix(Pigment $other, float $amount): Pigment { return $this->with( red: $this->red * (1 - $amount) + $other->red * $amount, @@ -83,15 +91,28 @@ record Pigment(int $red, int $yellow, int $blue) { blue: $this->blue * (1 - $amount) + $other->blue * $amount ); } + + // all properties are mutable in constructors + public function __construct() { + $this->red = max(0, min(255, $this->red)); + $this->yellow = max(0, min(255, $this->yellow)); + $this->blue = max(0, min(255, $this->blue)); + } + + public function with() { + // prevent the creation of a new Pigment from an existing pigment + throw new \LogicException("Cannot create a new Pigment from an existing pigment"); + } } +// simple records do not need to define a body record StockPaint(Pigment $color, float $volume); record PaintBucket(StockPaint ...$constituents) { public function mixIn(StockPaint $paint): PaintBucket { - return $this->with(...$this->constituents, $paint); + return $this->with(...[...$this->constituents, $paint]); } - + public function color(): Pigment { return array_reduce($this->constituents, fn($color, $paint) => $color->mix($paint->color, $paint->volume), Pigment(0, 0, 0)); } @@ -100,7 +121,7 @@ record PaintBucket(StockPaint ...$constituents) { === Usage === -A ''%%record%%'' may be used as a ''%%readonly class%%'', as the behavior of it is very similar with no key differences to assist in migration from ''%%readonly class%%''. +A ''%%record%%'' may be used any where a ''%%readonly class%%'' can be used, as the behavior of it is very similar with no key differences to assist in migration from ''%%readonly class%%''. === Optional parameters and default values === From f7c3a05dd687e3ba8ae8a8e6f32906cb0c7694a4 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Fri, 2 Aug 2024 10:50:25 +0200 Subject: [PATCH 18/46] address feedback and add more details --- drafts/records.md | 256 +++++++++++++++++++++++++++++++---------- published/records.ptxt | 194 ++++++++++++++++++++++++------- 2 files changed, 351 insertions(+), 99 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 7bd83ec..195f194 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -13,16 +13,18 @@ with [value semantics](https://en.wikipedia.org/wiki/Value_semantics). ### Value objects -Value objects are immutable objects that represent a value. They are used for storing values with a different meaning than +Value objects are immutable objects that represent a value. They’re used for storing values with a different meaning +than their technical value, adding additional semantic context to the value. 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. -Consider this example: +Consider this example where a function accepts an integer as a user ID, +and the id is accidentally set to a non-sensical value: ```php -function updateUserRole(int $userId, Role $role): void { +function updateUserRole(int $userId, string $role): void { // ... } @@ -31,24 +33,24 @@ $uid = $user->id; // ... $uid = 5; // somehow accidentally sets uid to an unrelated integer // ... -updateUserRole($uid, Role::ADMIN()); // accidental passing of +updateUserRole($uid, 'admin'); // accidental passing of a non-sensical value for uid ``` -In this example, the uid is accidentally set to a plain integer, and updateUserRole is called with the wrong value. - Currently, the only solution to this is to use a class, but this requires a lot of boilerplate code. +Further, `readonly` classes have a lot of edge cases and are rather unwieldy. #### The solution Like arrays, strings, and other values, `record` objects are strongly equal to each other if they contain the same values. -Let's take a look, using the previous example: +Let’s take a look at the updated example, using a `record` type for `UserId`. +Thus, if someone were to pass an `int` to `updateUserRole`, it would throw an error: ```php record UserId(int $id); -function updateUserRole(UserId $userId, Role $role): void { +function updateUserRole(UserId $userId, string $role): void { // ... } @@ -57,39 +59,47 @@ $uid = $user->id; // $uid is a UserId object // ... $uid = 5; // ... -updateUserRole($uid, Role::ADMIN()); // This will throw an error +updateUserRole($uid, 'admin'); // This will throw an error ``` Now, if `$uid` is accidentally set to an integer, -the call to `updateUserRole` will throw an error because the type is not correct. +the call to `updateUserRole` will throw a `TypeError` +because the function expects a `UserId` object instead of a plain integer. ## Proposal This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. -Records can implement interfaces and use traits but cannot extend other records or classes; +Records can implement interfaces and use traits but can’t extend other records or classes; composition is allowed, however. ### Syntax and semantics #### Definition -A `record` is defined by the word "record", followed by the name of its type, an open parenthesis containing one or more -typed parameters that become public, immutable, properties. -They may optionally implement an interface using the `implements` keyword. -A `record` body is optional. +A **record** is defined by the keyword `record`, +followed by the name of its type (e.g., `UserId`), +and then must list one or more typed parameters (e.g., `int $id`) that become properties of the record. +A parameter may provide `private` or `public` modifiers, but are `public` by default. +This is referred to as the "inline constructor." + +A **record** may optionally implement an interface using the `implements` keyword, +which may optionally be followed by a record body enclosed in curly braces `{}`. + +A **record** may not extend another record or class. + +A **record** may contain a traditional constructor with zero arguments to perform further initialization, +but if it does, it must take zero arguments. -A `record` may contain a constructor with zero arguments to perform further initialization if required. -If it does not have a constructor, an implicit, empty contstructor is provided. +A **record** body may contain property hooks, methods, and use traits. -A `record` body may contain property hooks, methods, and use traits (so long as they do not conflict with `record` -rules). -Regular properties may also be defined, but they are immutable by default and are no different from `const`. +A **record** body may also declare properties whose values are only mutable during a constructor call. +At any other time the property is immutable. -Static properties and methods are forbidden in a `record` (this includes -`const`, a regular property may be used instead). Attempting to define -static properties, methods, constants results in a compilation error. +A **record** body may also contain static methods and properties, +which behave identically to class static methods and properties. +They may be accessed using the `::` operator. ``` php namespace Paint; @@ -140,13 +150,19 @@ record PaintBucket(StockPaint ...$constituents) { #### Usage -A `record` may be used any where a `readonly class` can be used, -as the behavior of it is very similar with no key differences to assist in migration from `readonly class`. +A record may be used as a readonly class, +as the behavior of the two is very similar, +which should be able to assist in migrating from one implementation to another. #### Optional parameters and default values A `record` can also be defined with optional parameters that are set if omitted during instantiation. +One or more properties defined in the inline constructor may have a default value +declared using the same syntax and rules as any other default parameter declared in methods/functions. +If a property has a default value, +it is optional when instantiating the record and PHP will assign the default value to the property. + ``` php record Rectangle(int $x, int $y = 10); var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 @@ -158,24 +174,54 @@ To enhance the usability of records, the RFC proposes automatically generating a This method allows for partial updates of properties, creating a new instance of the record with the specified properties updated. -The auto-generated `with` method accepts only named arguments defined in the constructor. +The auto-generated `with` method accepts only named arguments defined in the constructor, +except variadic arguments. +For variadic arguments, these don’t require named arguments. No other property names can be used, and it returns a record object with the given values. +Example showing how this works with properties: + ``` php -$point1 = Point(3, 4); -$point2 = $point1->with(x: 5); -$point3 = $point1->with(null, 10); // must use named arguments +record UserId(int $id) { + public string $serialNumber; + + public function __construct() { + $this->serialNumber = "U{$this->id}"; + } +} -echo $point1->x; // Outputs: 3 -echo $point2->x; // Outputs: 5 +$userId = UserId(1); +$otherId = $userId->with(2); // failure due to not using named arguments +$otherId = $userId->with(serialNumber: "U2"); // serialNumber is not defined in the inline constructor +$otherId = $userId->with(id: 2); // success ``` +Example showing how this works with variadic arguments, +take note that PHP doesn’t allow sending variadic arguments with named arguments. +Thus, the developer may need +to perform multiple operations to construct a new record from an existing one using variadic arguments. + +```php +record Vector(int $dimensions, int ...$values); + +$vector = Vector(3, 1, 2, 3); +$vector = $vector->with(4, 5, 6); // automatically sent to $values +$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // not allowed by PHP syntax +$vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // set dimensions first, the set values. +``` + +This may look a bit confusing at first glance, +but PHP currently doesn’t allow mixing named arguments with variadic arguments. + +##### Custom with method + A developer may define their own `with` method if they so choose, and reference the generated `with` method using `parent::with()`. This allows a developer to define policies or constraints on how data is updated. ``` php record Planet(string $name, int $population) { + // create a with method that only accepts population updates public function with(int $population) { return parent::with(population: $population); } @@ -184,17 +230,22 @@ $pluto = Planet("Pluto", 0); // we made it! $pluto = $pluto->with(population: 1); // and then we changed the name -$mickey = $pluto->with(name: "Mickey"); // no named argument for population error +$mickey = $pluto->with(name: "Mickey"); // ERROR: no named argument for population ``` #### Constructors -Optionally, they may also define a constructor to provide validation or other initialization logic: +A **record** has two concepts of construction: the inline constructor and the traditional constructor. + +The inline constructor is always required and must define at least one parameter. +The traditional constructor is optional and can be used for further initialization logic; it must take zero arguments. ```php +// Inline constructor record User(string $name, string $email) { public string $id; + // Traditional constructor public function __construct() { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException("Invalid email address"); @@ -206,22 +257,63 @@ record User(string $name, string $email) { } ``` -During construction, a `record` is fully mutable. -This allows the developer freedom to mutate properties as needed to ensure a canonical representation of an object. +When a traditional constructor exists and is called, +the properties are already initialized to the value of the inline constructor +and are mutable until the end of the method, at which point they become immutable. + +### Mental models and how it works + +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 uses an existing object from an array. + +For example, this would be a valid mental model for a Point record: + +```php +record Point(int $x, int $y); + +// similar to declaring the following function and class + +class Point { + public int $x; + public int $y; + + public function __construct() {} +} + +function Point(int $x, int $y): Point { + static $points = []; + $key = "$x,$y"; + if ($points[$key] ?? null) { + return $points[$key]; + } + + $reflector = new \ReflectionClass(Point::class); + $point = $reflector->newInstanceWithoutConstructor(); + $point->x = $x; + $point->y = $y; + $point->__construct(); + $reflector->finalizeRecord($point); + return $points[$key] = $point; +} +``` + +In reality, it isn’t too much different than what actually happens in C, +except that the C version is more efficient and frees up memory when the object is no longer referenced. ### Performance considerations 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 are no -longer needed. +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. ``` php $point1 = Point(3, 4); $point2 = $point1; // No data duplication, $point2 references the same data as $point1 -$point3 = Point(3, 4); // No data duplication here either, it is pointing the the same memory as $point1 +$point3 = Point(3, 4); // No data duplication, it is pointing the the same memory as $point1 -$point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance with modified data +$point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance ``` #### Cloning and with() @@ -277,15 +369,15 @@ echo $time1 < $time2; // Outputs: true ### Reflection -Records in PHP will be fully supported by the reflection API, -providing access to their properties and methods just like regular classes. -However, immutability and special instantiation rules will be enforced. +Records can be interacted with via ReflectionClass, similar to readonly classes. +For instance, a developer can inspect private properties but not change them as records are immutable. + +Developers may create new instances of records using ReflectionFunction or ReflectionClass. More on this below. #### ReflectionClass support It can be used to inspect records, their properties, and methods. Any attempt to modify record properties -via reflection will throw an exception, maintaining immutability. Attempting to create a new instance via -`ReflectionClass` will cause a `ReflectionException` to be thrown. +via reflection will throw an exception, maintaining immutability. ``` php $point = Point(3, 4); @@ -298,7 +390,7 @@ foreach ($reflection->getProperties() as $property) { #### Immutability enforcement -Attempts to modify record properties via reflection will throw an exception. +Attempts to modify record properties via reflection will throw a `ReflectionException` exception. ``` php try { @@ -309,9 +401,9 @@ try { } ``` -#### ReflectionFunction for implicit constructor +#### ReflectionFunction for inline constructor -Using `ReflectionFunction` on a record will reflect the constructor. +Using `ReflectionFunction` on a record will reflect the inline constructor and can be used to construct new instances. ``` php $constructor = new \ReflectionFunction('Geometry\Point'); @@ -321,15 +413,55 @@ foreach ($constructor->getParameters() as $param) { } ``` -#### New functions and methods +#### Bypassing constructors + +During custom deserialization, a developer may need to bypass constructors to fill in properties. +Since records are mutable until the constructor is called, +this can be done by setting the properties before calling `finalizeRecord()`. + +Note that until `finalizeRecord()` is called, any guarantees of immutability and value semantics are **not enforced**. + +```php +record Point(int $x, int $y); + +$example = Point(1, 2); + +$pointReflector = new \ReflectionClass(Point::class); +// create a new point while keeping the properties mutable. +$point = $pointReflector->newInstanceWithoutConstructor(); +$point->x = 1; +$point->y = 2; +assert($example !== $point); // true +$pointReflector->finalizeRecord($point); +assert($example === $point); // true +``` + +Alternatively, a developer can use `ReflectionFunction` to get access to the inline constructor and call it directly: + +```php +record Point(int $x, int $y); + +$example = Point(1, 2); + +$pointReflector = new \ReflectionFunction(Point::class); +$point = $pointReflector->invoke(1, 2); + +assert($example === $point); // true +``` + +#### New and/or modified functions and methods - Calling `is_object($record)` will return `true`. -- A new function, `is_record($record)`, will return `true` for records, and `false` otherwise -- Calling `get_class($record)` will return the record name +- A new function, `is_record($record)`, will return `true` for records, and `false` otherwise. +- Calling `get_class($record)` will return the record name as a string. +- A new method, `ReflectionClass::finalizeRecord($instance)`, will be added to finalize a record, making it immutable. ### var_dump -Calling `var_dump` will look much like it does for objects, but instead of `object` it will say `record`. +When passed an instance of a record the `var_dump()` function will generate output the same +as if an equivalent object was passed — +e.g., both having the same properties — except the output generated will replace the prefix text "object" +with the text "record." ```txt record(Point)#1 (2) { @@ -347,14 +479,22 @@ creates both a `class` and a `function` with the same name. ### Autoloading -As invoking a record value by its name looks remarkably similar to calling a function, -and PHP has no function autoloader, autoloading will not be supported in this implementation. -If function autoloading were to be implemented in the future, an autoloader could locate the `record` and autoload it. -The author of this RFC strongly encourages someone to put forward a function autoloading RFC if autoloading is desired for records. +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. + +Once function autoloading is implemented in PHP at some hopeful point in the future, +said autoloader could locate the record and then autoload it. + +The author of this RFC strongly encourages someone to put forward a function autoloading RFC if autoloading is desired +for records. ## Backward Incompatible Changes -No backward incompatible changes. +To avoid conflicts with existing code, +the `record` keyword will be handled similarly to `enum` to prevent backward compatibility issues. ## Proposed PHP Version(s) @@ -394,7 +534,7 @@ None. ## Proposed Voting Choices -Include these so readers know where you are heading and can discuss the +Include these so readers know where you’re heading and can discuss the proposed voting options. ## Patches and Tests diff --git a/published/records.ptxt b/published/records.ptxt index 8263306..a36f3bf 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -12,12 +12,12 @@ This RFC proposes the introduction of ''%%record%%'' objects, which are immutabl ==== Value objects ==== -Value objects are immutable objects that represent a value. They are used for storing values with a different meaning than their technical value, adding additional semantic context to the value. 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. +Value objects are immutable objects that represent a value. They’re used for storing values with a different meaning than their technical value, adding additional semantic context to the value. 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. -Consider this example: +Consider this example where a function accepts an integer as a user ID, and the id is accidentally set to a non-sensical value: -function updateUserRole(int $userId, Role $role): void { +function updateUserRole(int $userId, string $role): void { // ... } @@ -26,23 +26,21 @@ $uid = $user->id; // ... $uid = 5; // somehow accidentally sets uid to an unrelated integer // ... -updateUserRole($uid, Role::ADMIN()); // accidental passing of +updateUserRole($uid, 'admin'); // accidental passing of a non-sensical value for uid -In this example, the uid is accidentally set to a plain integer, and updateUserRole is called with the wrong value. - -Currently, the only solution to this is to use a class, but this requires a lot of boilerplate code. +Currently, the only solution to this is to use a class, but this requires a lot of boilerplate code. Further, ''%%readonly%%'' classes have a lot of edge cases and are rather unwieldy. === The solution === Like arrays, strings, and other values, ''%%record%%'' objects are strongly equal to each other if they contain the same values. -Let's take a look, using the previous example: +Let’s take a look at the updated example, using a ''%%record%%'' type for ''%%UserId%%''. Thus, if someone were to pass an ''%%int%%'' to ''%%updateUserRole%%'', it would throw an error: record UserId(int $id); -function updateUserRole(UserId $userId, Role $role): void { +function updateUserRole(UserId $userId, string $role): void { // ... } @@ -51,26 +49,32 @@ $uid = $user->id; // $uid is a UserId object // ... $uid = 5; // ... -updateUserRole($uid, Role::ADMIN()); // This will throw an error +updateUserRole($uid, 'admin'); // This will throw an error -Now, if ''%%$uid%%'' is accidentally set to an integer, the call to ''%%updateUserRole%%'' will throw an error because the type is not correct. +Now, if ''%%$uid%%'' is accidentally set to an integer, the call to ''%%updateUserRole%%'' will throw a ''%%TypeError%%'' because the function expects a ''%%UserId%%'' object instead of a plain integer. ===== Proposal ===== -This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but cannot extend other records or classes; composition is allowed, however. +This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but can’t extend other records or classes; composition is allowed, however. ==== Syntax and semantics ==== === Definition === -A ''%%record%%'' is defined by the word "record", followed by the name of its type, an open parenthesis containing one or more typed parameters that become public, immutable, properties. They may optionally implement an interface using the ''%%implements%%'' keyword. A ''%%record%%'' body is optional. +A **record** is defined by the keyword ''%%record%%'', followed by the name of its type (e.g., ''%%UserId%%''), and then must list one or more typed parameters (e.g., ''%%int $id%%'') that become properties of the record. A parameter may provide ''%%private%%'' or ''%%public%%'' modifiers, but are ''%%public%%'' by default. This is referred to as the "inline constructor." + +A **record** may optionally implement an interface using the ''%%implements%%'' keyword, which may optionally be followed by a record body enclosed in curly braces ''%%{}%%''. + +A **record** may not extend another record or class. + +A **record** may contain a traditional constructor with zero arguments to perform further initialization, but if it does, it must take zero arguments. -A ''%%record%%'' may contain a constructor with zero arguments to perform further initialization if required. If it does not have a constructor, an implicit, empty contstructor is provided. +A **record** body may contain property hooks, methods, and use traits. -A ''%%record%%'' body may contain property hooks, methods, and use traits (so long as they do not conflict with ''%%record%%'' rules). Regular properties may also be defined, but they are immutable by default and are no different from ''%%const%%''. +A **record** body may also declare properties whose values are only mutable during a constructor call. At any other time the property is immutable. -Static properties and methods are forbidden in a ''%%record%%'' (this includes ''%%const%%'', a regular property may be used instead). Attempting to define static properties, methods, constants results in a compilation error. +A **record** body may also contain static methods and properties, which behave identically to class static methods and properties. They may be accessed using the ''%%::%%'' operator. namespace Paint; @@ -121,12 +125,14 @@ record PaintBucket(StockPaint ...$constituents) { === Usage === -A ''%%record%%'' may be used any where a ''%%readonly class%%'' can be used, as the behavior of it is very similar with no key differences to assist in migration from ''%%readonly class%%''. +A record may be used as a readonly class, as the behavior of the two is very similar, which should be able to assist in migrating from one implementation to another. === Optional parameters and default values === A ''%%record%%'' can also be defined with optional parameters that are set if omitted during instantiation. +One or more properties defined in the inline constructor may have a default value declared using the same syntax and rules as any other default parameter declared in methods/functions. If a property has a default value, it is optional when instantiating the record and PHP will assign the default value to the property. + record Rectangle(int $x, int $y = 10); var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 @@ -136,21 +142,45 @@ var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 To enhance the usability of records, the RFC proposes automatically generating a ''%%with%%'' method for each record. This method allows for partial updates of properties, creating a new instance of the record with the specified properties updated. -The auto-generated ''%%with%%'' method accepts only named arguments defined in the constructor. No other property names can be used, and it returns a record object with the given values. +The auto-generated ''%%with%%'' method accepts only named arguments defined in the constructor, except variadic arguments. For variadic arguments, these don’t require named arguments. No other property names can be used, and it returns a record object with the given values. + +Example showing how this works with properties: -$point1 = Point(3, 4); -$point2 = $point1->with(x: 5); -$point3 = $point1->with(null, 10); // must use named arguments +record UserId(int $id) { + public string $serialNumber; + + public function __construct() { + $this->serialNumber = "U{$this->id}"; + } +} -echo $point1->x; // Outputs: 3 -echo $point2->x; // Outputs: 5 +$userId = UserId(1); +$otherId = $userId->with(2); // failure due to not using named arguments +$otherId = $userId->with(serialNumber: "U2"); // serialNumber is not defined in the inline constructor +$otherId = $userId->with(id: 2); // success +Example showing how this works with variadic arguments, take note that PHP doesn’t allow sending variadic arguments with named arguments. Thus, the developer may need to perform multiple operations to construct a new record from an existing one using variadic arguments. + + +record Vector(int $dimensions, int ...$values); + +$vector = Vector(3, 1, 2, 3); +$vector = $vector->with(4, 5, 6); // automatically sent to $values +$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // not allowed by PHP syntax +$vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // set dimensions first, the set values. + + +This may look a bit confusing at first glance, but PHP currently doesn’t allow mixing named arguments with variadic arguments. + +== Custom with method == + A developer may define their own ''%%with%%'' method if they so choose, and reference the generated ''%%with%%'' method using ''%%parent::with()%%''. This allows a developer to define policies or constraints on how data is updated. record Planet(string $name, int $population) { + // create a with method that only accepts population updates public function with(int $population) { return parent::with(population: $population); } @@ -159,17 +189,21 @@ $pluto = Planet("Pluto", 0); // we made it! $pluto = $pluto->with(population: 1); // and then we changed the name -$mickey = $pluto->with(name: "Mickey"); // no named argument for population error +$mickey = $pluto->with(name: "Mickey"); // ERROR: no named argument for population === Constructors === -Optionally, they may also define a constructor to provide validation or other initialization logic: +A **record** has two concepts of construction: the inline constructor and the traditional constructor. + +The inline constructor is always required and must define at least one parameter. The traditional constructor is optional and can be used for further initialization logic; it must take zero arguments. +// Inline constructor record User(string $name, string $email) { public string $id; + // Traditional constructor public function __construct() { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException("Invalid email address"); @@ -181,18 +215,55 @@ record User(string $name, string $email) { } -During construction, a ''%%record%%'' is fully mutable. This allows the developer freedom to mutate properties as needed to ensure a canonical representation of an object. +When a traditional constructor exists and is called, the properties are already initialized to the value of the inline constructor and are mutable until the end of the method, at which point they become immutable. + +==== Mental models and how it works ==== + +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 uses an existing object from an array. + +For example, this would be a valid mental model for a Point record: + + +record Point(int $x, int $y); + +// similar to declaring the following function and class + +class Point { + public int $x; + public int $y; + + public function __construct() {} +} + +function Point(int $x, int $y): Point { + static $points = []; + $key = "$x,$y"; + if ($points[$key] ?? null) { + return $points[$key]; + } + + $reflector = new \ReflectionClass(Point::class); + $point = $reflector->newInstanceWithoutConstructor(); + $point->x = $x; + $point->y = $y; + $point->__construct(); + $reflector->finalizeRecord($point); + return $points[$key] = $point; +} + + +In reality, it isn’t too much different than what actually happens in C, except that the C version is more efficient and frees up memory when the object is no longer referenced. ==== Performance considerations ==== -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 are no longer needed. +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. $point1 = Point(3, 4); $point2 = $point1; // No data duplication, $point2 references the same data as $point1 -$point3 = Point(3, 4); // No data duplication here either, it is pointing the the same memory as $point1 +$point3 = Point(3, 4); // No data duplication, it is pointing the the same memory as $point1 -$point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance with modified data +$point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance === Cloning and with() === @@ -242,11 +313,13 @@ echo $time1 < $time2; // Outputs: true ==== Reflection ==== -Records in PHP will be fully supported by the reflection API, providing access to their properties and methods just like regular classes. However, immutability and special instantiation rules will be enforced. +Records can be interacted with via ReflectionClass, similar to readonly classes. For instance, a developer can inspect private properties but not change them as records are immutable. + +Developers may create new instances of records using ReflectionFunction or ReflectionClass. More on this below. === ReflectionClass support === -It can be used to inspect records, their properties, and methods. Any attempt to modify record properties via reflection will throw an exception, maintaining immutability. Attempting to create a new instance via ''%%ReflectionClass%%'' will cause a ''%%ReflectionException%%'' to be thrown. +It can be used to inspect records, their properties, and methods. Any attempt to modify record properties via reflection will throw an exception, maintaining immutability. $point = Point(3, 4); @@ -259,7 +332,7 @@ foreach ($reflection->getProperties() as $property) { === Immutability enforcement === -Attempts to modify record properties via reflection will throw an exception. +Attempts to modify record properties via reflection will throw a ''%%ReflectionException%%'' exception. try { @@ -270,9 +343,9 @@ try { } -=== ReflectionFunction for implicit constructor === +=== ReflectionFunction for inline constructor === -Using ''%%ReflectionFunction%%'' on a record will reflect the constructor. +Using ''%%ReflectionFunction%%'' on a record will reflect the inline constructor and can be used to construct new instances. $constructor = new \ReflectionFunction('Geometry\Point'); @@ -282,15 +355,50 @@ foreach ($constructor->getParameters() as $param) { } -=== New functions and methods === +=== Bypassing constructors === + +During custom deserialization, a developer may need to bypass constructors to fill in properties. Since records are mutable until the constructor is called, this can be done by setting the properties before calling ''%%finalizeRecord()%%''. + +Note that until ''%%finalizeRecord()%%'' is called, any guarantees of immutability and value semantics are **not enforced**. + + +record Point(int $x, int $y); + +$example = Point(1, 2); + +$pointReflector = new \ReflectionClass(Point::class); +// create a new point while keeping the properties mutable. +$point = $pointReflector->newInstanceWithoutConstructor(); +$point->x = 1; +$point->y = 2; +assert($example !== $point); // true +$pointReflector->finalizeRecord($point); +assert($example === $point); // true + + +Alternatively, a developer can use ''%%ReflectionFunction%%'' to get access to the inline constructor and call it directly: + + +record Point(int $x, int $y); + +$example = Point(1, 2); + +$pointReflector = new \ReflectionFunction(Point::class); +$point = $pointReflector->invoke(1, 2); + +assert($example === $point); // true + + +=== New and/or modified functions and methods === * Calling ''%%is_object($record)%%'' will return ''%%true%%''. - * A new function, ''%%is_record($record)%%'', will return ''%%true%%'' for records, and ''%%false%%'' otherwise - * Calling ''%%get_class($record)%%'' will return the record name + * A new function, ''%%is_record($record)%%'', will return ''%%true%%'' for records, and ''%%false%%'' otherwise. + * Calling ''%%get_class($record)%%'' will return the record name as a string. + * A new method, ''%%ReflectionClass::finalizeRecord($instance)%%'', will be added to finalize a record, making it immutable. ==== var_dump ==== -Calling ''%%var_dump%%'' will look much like it does for objects, but instead of ''%%object%%'' it will say ''%%record%%''. +When passed an instance of a record the ''%%var_dump()%%'' function will generate output the same as if an equivalent object was passed — e.g., both having the same properties — except the output generated will replace the prefix text "object" with the text "record." record(Point)#1 (2) { @@ -307,11 +415,15 @@ A ''%%record%%'' cannot be named after an existing ''%%record%%'', ''%%class%%'' ==== Autoloading ==== -As invoking a record value by its name looks remarkably similar to calling a function, and PHP has no function autoloader, autoloading will not be supported in this implementation. If function autoloading were to be implemented in the future, an autoloader could locate the ''%%record%%'' and autoload it. The author of this RFC strongly encourages someone to put forward a function autoloading RFC if autoloading is desired for records. +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. + +Once function autoloading is implemented in PHP at some hopeful point in the future, said autoloader could locate the record and then autoload it. + +The author of this RFC strongly encourages someone to put forward a function autoloading RFC if autoloading is desired for records. ===== Backward Incompatible Changes ===== -No backward incompatible changes. +To avoid conflicts with existing code, the ''%%record%%'' keyword will be handled similarly to ''%%enum%%'' to prevent backward compatibility issues. ===== Proposed PHP Version(s) ===== @@ -351,7 +463,7 @@ None. ===== Proposed Voting Choices ===== -Include these so readers know where you are heading and can discuss the proposed voting options. +Include these so readers know where you’re heading and can discuss the proposed voting options. ===== Patches and Tests ===== From a2718a6bfd0a9de01414c97f70a4ff0007640c78 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Fri, 2 Aug 2024 11:10:25 +0200 Subject: [PATCH 19/46] address feedback --- drafts/records.md | 14 +++++++------- published/records.ptxt | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 195f194..098670f 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -21,7 +21,7 @@ and an `ExpirationDate` can represent a date when something expires. This prevents developers from accidentally using the wrong value in the wrong context. Consider this example where a function accepts an integer as a user ID, -and the id is accidentally set to a non-sensical value: +and the ID is accidentally set to a nonsensical value: ```php function updateUserRole(int $userId, string $role): void { @@ -37,7 +37,7 @@ updateUserRole($uid, 'admin'); // accidental passing of a non-sensical value for ``` Currently, the only solution to this is to use a class, but this requires a lot of boilerplate code. -Further, `readonly` classes have a lot of edge cases and are rather unwieldy. +Further, `readonly` classes have numerous edge cases and are rather unwieldy. #### The solution @@ -68,7 +68,7 @@ because the function expects a `UserId` object instead of a plain integer. ## Proposal -This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will +This RFC proposes the introduction of a `record` keyword in PHP to define immutable data objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but can’t extend other records or classes; @@ -95,7 +95,7 @@ but if it does, it must take zero arguments. A **record** body may contain property hooks, methods, and use traits. A **record** body may also declare properties whose values are only mutable during a constructor call. -At any other time the property is immutable. +At any other time, the property is immutable. A **record** body may also contain static methods and properties, which behave identically to class static methods and properties. @@ -199,7 +199,7 @@ $otherId = $userId->with(id: 2); // success Example showing how this works with variadic arguments, take note that PHP doesn’t allow sending variadic arguments with named arguments. Thus, the developer may need -to perform multiple operations to construct a new record from an existing one using variadic arguments. +to perform multiple operations to construct a record from an existing one using variadic arguments. ```php record Vector(int $dimensions, int ...$values); @@ -370,7 +370,7 @@ echo $time1 < $time2; // Outputs: true ### Reflection Records can be interacted with via ReflectionClass, similar to readonly classes. -For instance, a developer can inspect private properties but not change them as records are immutable. +For instance, a developer can inspect private properties but not change them, as records are immutable. Developers may create new instances of records using ReflectionFunction or ReflectionClass. More on this below. @@ -459,7 +459,7 @@ assert($example === $point); // true ### var_dump When passed an instance of a record the `var_dump()` function will generate output the same -as if an equivalent object was passed — +as if an equivalent object were passed — e.g., both having the same properties — except the output generated will replace the prefix text "object" with the text "record." diff --git a/published/records.ptxt b/published/records.ptxt index a36f3bf..cb9cdc1 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -14,7 +14,7 @@ This RFC proposes the introduction of ''%%record%%'' objects, which are immutabl Value objects are immutable objects that represent a value. They’re used for storing values with a different meaning than their technical value, adding additional semantic context to the value. 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. -Consider this example where a function accepts an integer as a user ID, and the id is accidentally set to a non-sensical value: +Consider this example where a function accepts an integer as a user ID, and the ID is accidentally set to a nonsensical value: function updateUserRole(int $userId, string $role): void { @@ -29,7 +29,7 @@ $uid = 5; // somehow accidentally sets uid to an unrelated integer updateUserRole($uid, 'admin'); // accidental passing of a non-sensical value for uid -Currently, the only solution to this is to use a class, but this requires a lot of boilerplate code. Further, ''%%readonly%%'' classes have a lot of edge cases and are rather unwieldy. +Currently, the only solution to this is to use a class, but this requires a lot of boilerplate code. Further, ''%%readonly%%'' classes have numerous edge cases and are rather unwieldy. === The solution === @@ -56,7 +56,7 @@ Now, if ''%%$uid%%'' is accidentally set to an integer, the call to ''%%updateUs ===== Proposal ===== -This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but can’t extend other records or classes; composition is allowed, however. +This RFC proposes the introduction of a ''%%record%%'' keyword in PHP to define immutable data objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but can’t extend other records or classes; composition is allowed, however. ==== Syntax and semantics ==== @@ -72,7 +72,7 @@ A **record** may contain a traditional constructor with zero arguments to perfor A **record** body may contain property hooks, methods, and use traits. -A **record** body may also declare properties whose values are only mutable during a constructor call. At any other time the property is immutable. +A **record** body may also declare properties whose values are only mutable during a constructor call. At any other time, the property is immutable. A **record** body may also contain static methods and properties, which behave identically to class static methods and properties. They may be accessed using the ''%%::%%'' operator. @@ -161,7 +161,7 @@ $otherId = $userId->with(serialNumber: "U2"); // serialNumber is not defined in $otherId = $userId->with(id: 2); // success -Example showing how this works with variadic arguments, take note that PHP doesn’t allow sending variadic arguments with named arguments. Thus, the developer may need to perform multiple operations to construct a new record from an existing one using variadic arguments. +Example showing how this works with variadic arguments, take note that PHP doesn’t allow sending variadic arguments with named arguments. Thus, the developer may need to perform multiple operations to construct a record from an existing one using variadic arguments. record Vector(int $dimensions, int ...$values); @@ -313,7 +313,7 @@ echo $time1 < $time2; // Outputs: true ==== Reflection ==== -Records can be interacted with via ReflectionClass, similar to readonly classes. For instance, a developer can inspect private properties but not change them as records are immutable. +Records can be interacted with via ReflectionClass, similar to readonly classes. For instance, a developer can inspect private properties but not change them, as records are immutable. Developers may create new instances of records using ReflectionFunction or ReflectionClass. More on this below. @@ -398,7 +398,7 @@ assert($example === $point); // true ==== var_dump ==== -When passed an instance of a record the ''%%var_dump()%%'' function will generate output the same as if an equivalent object was passed — e.g., both having the same properties — except the output generated will replace the prefix text "object" with the text "record." +When passed an instance of a record the ''%%var_dump()%%'' function will generate output the same as if an equivalent object were passed — e.g., both having the same properties — except the output generated will replace the prefix text "object" with the text "record." record(Point)#1 (2) { From 714a44578ef867a20a91a1e833703d02330d6e9e Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Fri, 2 Aug 2024 11:34:45 +0200 Subject: [PATCH 20/46] grammar --- drafts/records.md | 31 ++++++++++++++++--------------- published/records.ptxt | 22 +++++++++++----------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 098670f..f687489 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -13,9 +13,9 @@ with [value semantics](https://en.wikipedia.org/wiki/Value_semantics). ### Value objects -Value objects are immutable objects that represent a value. They’re used for storing values with a different meaning -than -their technical value, adding additional semantic context to the value. +Value objects are immutable objects that represent a value. +They’re used to store values with a different meaning than their technical value, +adding additional semantic context to the value. 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. @@ -36,7 +36,7 @@ $uid = 5; // somehow accidentally sets uid to an unrelated integer updateUserRole($uid, 'admin'); // accidental passing of a non-sensical value for uid ``` -Currently, the only solution to this is to use a class, but this requires a lot of boilerplate code. +Currently, the only solution to this is to use a class, but this requires significant boilerplate code. Further, `readonly` classes have numerous edge cases and are rather unwieldy. #### The solution @@ -152,7 +152,7 @@ record PaintBucket(StockPaint ...$constituents) { A record may be used as a readonly class, as the behavior of the two is very similar, -which should be able to assist in migrating from one implementation to another. +assisting in migrating from one implementation to another. #### Optional parameters and default values @@ -192,7 +192,7 @@ record UserId(int $id) { $userId = UserId(1); $otherId = $userId->with(2); // failure due to not using named arguments -$otherId = $userId->with(serialNumber: "U2"); // serialNumber is not defined in the inline constructor +$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not defined in the inline constructor $otherId = $userId->with(id: 2); // success ``` @@ -206,7 +206,7 @@ record Vector(int $dimensions, int ...$values); $vector = Vector(3, 1, 2, 3); $vector = $vector->with(4, 5, 6); // automatically sent to $values -$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // not allowed by PHP syntax +$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: mixing named arguments with variadic arguments is not allowed by PHP syntax $vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // set dimensions first, the set values. ``` @@ -230,7 +230,7 @@ $pluto = Planet("Pluto", 0); // we made it! $pluto = $pluto->with(population: 1); // and then we changed the name -$mickey = $pluto->with(name: "Mickey"); // ERROR: no named argument for population +$mickey = $pluto->with(name: "Mickey"); // Error: no named argument for population ``` #### Constructors @@ -238,7 +238,7 @@ $mickey = $pluto->with(name: "Mickey"); // ERROR: no named argument for populati A **record** has two concepts of construction: the inline constructor and the traditional constructor. The inline constructor is always required and must define at least one parameter. -The traditional constructor is optional and can be used for further initialization logic; it must take zero arguments. +The traditional constructor is optional and can be used for further initialization logic. ```php // Inline constructor @@ -265,7 +265,7 @@ and are mutable until the end of the method, at which point they become immutabl 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 uses an existing object from an array. +as a factory function that creates a new object or retrieves an existing object from an array. For example, this would be a valid mental model for a Point record: @@ -311,7 +311,7 @@ longer necessary. ``` php $point1 = Point(3, 4); $point2 = $point1; // No data duplication, $point2 references the same data as $point1 -$point3 = Point(3, 4); // No data duplication, it is pointing the the same memory as $point1 +$point3 = Point(3, 4); // No data duplication, it is pointing to the same memory as $point1 $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance ``` @@ -376,8 +376,9 @@ Developers may create new instances of records using ReflectionFunction or Refle #### ReflectionClass support -It can be used to inspect records, their properties, and methods. Any attempt to modify record properties -via reflection will throw an exception, maintaining immutability. +It can be used to inspect records, their properties, and methods. +Any attempt to modify finalized record properties via reflection will throw a `ReflectionException` exception, +maintaining immutability. ``` php $point = Point(3, 4); @@ -474,8 +475,8 @@ record(Point)#1 (2) { ### Considerations for implementations -A `record` cannot be named after an existing `record`, `class` or `function`. This is because defining a `record` -creates both a `class` and a `function` with the same name. +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. ### Autoloading diff --git a/published/records.ptxt b/published/records.ptxt index cb9cdc1..33879eb 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -12,7 +12,7 @@ This RFC proposes the introduction of ''%%record%%'' objects, which are immutabl ==== Value objects ==== -Value objects are immutable objects that represent a value. They’re used for storing values with a different meaning than their technical value, adding additional semantic context to the value. 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. +Value objects are immutable objects that represent a value. They’re used to store values with a different meaning than their technical value, adding additional semantic context to the value. 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. Consider this example where a function accepts an integer as a user ID, and the ID is accidentally set to a nonsensical value: @@ -29,7 +29,7 @@ $uid = 5; // somehow accidentally sets uid to an unrelated integer updateUserRole($uid, 'admin'); // accidental passing of a non-sensical value for uid -Currently, the only solution to this is to use a class, but this requires a lot of boilerplate code. Further, ''%%readonly%%'' classes have numerous edge cases and are rather unwieldy. +Currently, the only solution to this is to use a class, but this requires significant boilerplate code. Further, ''%%readonly%%'' classes have numerous edge cases and are rather unwieldy. === The solution === @@ -125,7 +125,7 @@ record PaintBucket(StockPaint ...$constituents) { === Usage === -A record may be used as a readonly class, as the behavior of the two is very similar, which should be able to assist in migrating from one implementation to another. +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. === Optional parameters and default values === @@ -157,7 +157,7 @@ record UserId(int $id) { $userId = UserId(1); $otherId = $userId->with(2); // failure due to not using named arguments -$otherId = $userId->with(serialNumber: "U2"); // serialNumber is not defined in the inline constructor +$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not defined in the inline constructor $otherId = $userId->with(id: 2); // success @@ -168,7 +168,7 @@ record Vector(int $dimensions, int ...$values); $vector = Vector(3, 1, 2, 3); $vector = $vector->with(4, 5, 6); // automatically sent to $values -$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // not allowed by PHP syntax +$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: mixing named arguments with variadic arguments is not allowed by PHP syntax $vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // set dimensions first, the set values. @@ -189,14 +189,14 @@ $pluto = Planet("Pluto", 0); // we made it! $pluto = $pluto->with(population: 1); // and then we changed the name -$mickey = $pluto->with(name: "Mickey"); // ERROR: no named argument for population +$mickey = $pluto->with(name: "Mickey"); // Error: no named argument for population === Constructors === A **record** has two concepts of construction: the inline constructor and the traditional constructor. -The inline constructor is always required and must define at least one parameter. The traditional constructor is optional and can be used for further initialization logic; it must take zero arguments. +The inline constructor is always required and must define at least one parameter. The traditional constructor is optional and can be used for further initialization logic. // Inline constructor @@ -219,7 +219,7 @@ When a traditional constructor exists and is called, the properties are already ==== Mental models and how it works ==== -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 uses an existing object from an array. +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. For example, this would be a valid mental model for a Point record: @@ -261,7 +261,7 @@ To ensure that records are both performant and memory-efficient, the RFC propose $point1 = Point(3, 4); $point2 = $point1; // No data duplication, $point2 references the same data as $point1 -$point3 = Point(3, 4); // No data duplication, it is pointing the the same memory as $point1 +$point3 = Point(3, 4); // No data duplication, it is pointing to the same memory as $point1 $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance @@ -319,7 +319,7 @@ Developers may create new instances of records using ReflectionFunction or Refle === ReflectionClass support === -It can be used to inspect records, their properties, and methods. Any attempt to modify record properties via reflection will throw an exception, maintaining immutability. +It can be used to inspect records, their properties, and methods. Any attempt to modify finalized record properties via reflection will throw a ''%%ReflectionException%%'' exception, maintaining immutability. $point = Point(3, 4); @@ -411,7 +411,7 @@ record(Point)#1 (2) { ==== Considerations for implementations ==== -A ''%%record%%'' cannot be named after an existing ''%%record%%'', ''%%class%%'' or ''%%function%%''. This is because defining a ''%%record%%'' creates both a ''%%class%%'' and a ''%%function%%'' with the same name. +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. ==== Autoloading ==== From 12d702d4c9ca61e38fe2cb95b68ae16d0487827c Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Fri, 2 Aug 2024 12:44:17 +0200 Subject: [PATCH 21/46] clarity --- drafts/records.md | 134 +++++++++++++++++++++++++---------------- published/records.ptxt | 103 ++++++++++++++++++++----------- 2 files changed, 149 insertions(+), 88 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index f687489..4ea6068 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -14,8 +14,7 @@ with [value semantics](https://en.wikipedia.org/wiki/Value_semantics). ### Value objects Value objects are immutable objects that represent a value. -They’re used to store values with a different meaning than their technical value, -adding additional semantic context to the 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. @@ -33,15 +32,15 @@ $uid = $user->id; // ... $uid = 5; // somehow accidentally sets uid to an unrelated integer // ... -updateUserRole($uid, 'admin'); // accidental passing of a non-sensical value for uid +updateUserRole($uid, 'admin'); // accidental passing of a nonsensical value for uid ``` -Currently, the only solution to this is to use a class, but this requires significant boilerplate code. -Further, `readonly` classes have numerous edge cases and are rather unwieldy. +Currently, the only solution to this is to use a **class**, but this requires significant boilerplate code. +Further, **readonly classes** have many edge cases and are rather unwieldy. #### The solution -Like arrays, strings, and other values, `record` objects are strongly equal to each other if they contain the same +Like arrays, strings, and other values, **record** objects are strongly equal (`===`) to each other if they contain the same values. Let’s take a look at the updated example, using a `record` type for `UserId`. @@ -68,9 +67,10 @@ because the function expects a `UserId` object instead of a plain integer. ## Proposal -This RFC proposes the introduction of a `record` keyword in PHP to define immutable data objects. These objects will -allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying -properties and equality checks using a function-like instantiation syntax. +This RFC proposes the introduction of a `record` keyword in PHP to define immutable value objects. +These objects will allow properties to be initialized concisely +and will provide built-in methods for common operations +such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but can’t extend other records or classes; composition is allowed, however. @@ -81,7 +81,7 @@ composition is allowed, however. A **record** is defined by the keyword `record`, followed by the name of its type (e.g., `UserId`), and then must list one or more typed parameters (e.g., `int $id`) that become properties of the record. -A parameter may provide `private` or `public` modifiers, but are `public` by default. +A parameter may provide `private` or `public` modifiers, but are `public` by when not specified. This is referred to as the "inline constructor." A **record** may optionally implement an interface using the `implements` keyword, @@ -89,8 +89,7 @@ which may optionally be followed by a record body enclosed in curly braces `{}`. A **record** may not extend another record or class. -A **record** may contain a traditional constructor with zero arguments to perform further initialization, -but if it does, it must take zero arguments. +A **record** may contain a traditional constructor with zero arguments to perform further initialization. A **record** body may contain property hooks, methods, and use traits. @@ -98,7 +97,7 @@ A **record** body may also declare properties whose values are only mutable duri At any other time, the property is immutable. A **record** body may also contain static methods and properties, -which behave identically to class static methods and properties. +which behave identically to static methods and properties in classes. They may be accessed using the `::` operator. ``` php @@ -151,7 +150,7 @@ record PaintBucket(StockPaint ...$constituents) { #### Usage A record may be used as a readonly class, -as the behavior of the two is very similar, +as the behavior of the two is very similar once instantiated, assisting in migrating from one implementation to another. #### Optional parameters and default values @@ -161,7 +160,7 @@ A `record` can also be defined with optional parameters that are set if omitted One or more properties defined in the inline constructor may have a default value declared using the same syntax and rules as any other default parameter declared in methods/functions. If a property has a default value, -it is optional when instantiating the record and PHP will assign the default value to the property. +it is optional when instantiating the record, and PHP will assign the default value to the property. ``` php record Rectangle(int $x, int $y = 10); @@ -170,59 +169,64 @@ var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 #### Auto-generated `with` method -To enhance the usability of records, the RFC proposes automatically generating a `with` method for each record. -This method allows for partial updates of properties, creating a new instance of the record with the specified -properties updated. +To make records more useful, the RFC proposes generating a `with` method for each record. +This method allows for partial updates to the properties, +creating a new instance of the record with the specified properties updated. -The auto-generated `with` method accepts only named arguments defined in the constructor, -except variadic arguments. -For variadic arguments, these don’t require named arguments. -No other property names can be used, and it returns a record object with the given values. +##### How the with method works -Example showing how this works with properties: +**Named arguments** -``` php +The `with` method accepts only named arguments defined in the inline constructor. +Properties not defined in the inline constructor can’t be updated by this method. + +**Variadic arguments** + +Variadic arguments from the inline constructor don’t require named arguments in the `with` method. +However, mixing variadic arguments in the same `with` method call is not allowed by PHP syntax. + +Using named arguments: + +```php record UserId(int $id) { public string $serialNumber; - + public function __construct() { $this->serialNumber = "U{$this->id}"; } } $userId = UserId(1); -$otherId = $userId->with(2); // failure due to not using named arguments -$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not defined in the inline constructor -$otherId = $userId->with(id: 2); // success +$otherId = $userId->with(2); // Fails: Named arguments must be used +$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not in the inline constructor +$otherId = $userId->with(id: 2); // Success: id is updated ``` -Example showing how this works with variadic arguments, -take note that PHP doesn’t allow sending variadic arguments with named arguments. -Thus, the developer may need -to perform multiple operations to construct a record from an existing one using variadic arguments. +Using variadic arguments: ```php record Vector(int $dimensions, int ...$values); $vector = Vector(3, 1, 2, 3); -$vector = $vector->with(4, 5, 6); // automatically sent to $values -$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: mixing named arguments with variadic arguments is not allowed by PHP syntax -$vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // set dimensions first, the set values. +$vector = $vector->with(dimensions: 4); // Success: values are updated +$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: Mixing named and variadic arguments +$vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // Success: First update dimensions, then values ``` -This may look a bit confusing at first glance, -but PHP currently doesn’t allow mixing named arguments with variadic arguments. - ##### Custom with method A developer may define their own `with` method if they so choose, and reference the generated `with` method using `parent::with()`. This allows a developer to define policies or constraints on how data is updated. +Contravariance and covariance are enforced in the developer’s code: +- Contravariance: the parameter type of the custom `with` method must be a supertype of the generated `with` method. +- Covariance: the return type of the custom `with` method must be `self` of the generated `with` method. + ``` php record Planet(string $name, int $population) { // create a with method that only accepts population updates - public function with(int $population) { + public function with(int $population): Planet { return parent::with(population: $population); } } @@ -235,10 +239,15 @@ $mickey = $pluto->with(name: "Mickey"); // Error: no named argument for populati #### Constructors -A **record** has two concepts of construction: the inline constructor and the traditional constructor. +A **record** has two types of constructors: the inline constructor and the traditional constructor. The inline constructor is always required and must define at least one parameter. -The traditional constructor is optional and can be used for further initialization logic. +The traditional constructor is optional and can be used for further initialization logic, +but must not accept any arguments. + +When a traditional constructor exists and is called, +the properties are already initialized to the value of the inline constructor +and are mutable until the end of the method, at which point they become immutable. ```php // Inline constructor @@ -257,10 +266,6 @@ record User(string $name, string $email) { } ``` -When a traditional constructor exists and is called, -the properties are already initialized to the value of the inline constructor -and are mutable until the end of the method, at which point they become immutable. - ### Mental models and how it works From the perspective of a developer, declaring a record declares an object and function with the same name. @@ -274,32 +279,55 @@ record Point(int $x, int $y); // similar to declaring the following function and class -class Point { +// used during construction to allow immutability +class Point_Implementation { public int $x; public int $y; - + public function __construct() {} + + public function with(...$parameters) { + // validity checks omitted for brevity + $parameters = array_merge([$this->x, $this->y], $parameters); + return Point(...$parameters); + } +} + +// used to enforce immutability but has the same implementation +readonly class Point { + public function __construct(public int $x, public int $y) {} + + public function with(...$parameters) { + // validity checks omitted for brevity + $parameters = array_merge([$this->x, $this->y], $parameters); + return Point(...$parameters); + } } function Point(int $x, int $y): Point { static $points = []; - $key = "$x,$y"; + // look up the identity of the point + $key = hash_func($x, $y); if ($points[$key] ?? null) { + // return an existing point return $points[$key]; } - - $reflector = new \ReflectionClass(Point::class); + + // create a new point + $reflector = new \ReflectionClass(Point_Implementation::class); $point = $reflector->newInstanceWithoutConstructor(); $point->x = $x; $point->y = $y; $point->__construct(); - $reflector->finalizeRecord($point); + // copy properties to an immutable point and return it + $point = new Point($point->x, $point->y); return $points[$key] = $point; } ``` -In reality, it isn’t too much different than what actually happens in C, -except that the C version is more efficient and frees up memory when the object is no longer referenced. +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. ### Performance considerations diff --git a/published/records.ptxt b/published/records.ptxt index 33879eb..de9a3b6 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -12,7 +12,7 @@ This RFC proposes the introduction of ''%%record%%'' objects, which are immutabl ==== Value objects ==== -Value objects are immutable objects that represent a value. They’re used to store values with a different meaning than their technical value, adding additional semantic context to the value. 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. +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. Consider this example where a function accepts an integer as a user ID, and the ID is accidentally set to a nonsensical value: @@ -26,14 +26,14 @@ $uid = $user->id; // ... $uid = 5; // somehow accidentally sets uid to an unrelated integer // ... -updateUserRole($uid, 'admin'); // accidental passing of a non-sensical value for uid +updateUserRole($uid, 'admin'); // accidental passing of a nonsensical value for uid -Currently, the only solution to this is to use a class, but this requires significant boilerplate code. Further, ''%%readonly%%'' classes have numerous edge cases and are rather unwieldy. +Currently, the only solution to this is to use a **class**, but this requires significant boilerplate code. Further, **readonly classes** have many edge cases and are rather unwieldy. === The solution === -Like arrays, strings, and other values, ''%%record%%'' objects are strongly equal to each other if they contain the same values. +Like arrays, strings, and other values, **record** objects are strongly equal (''%%===%%'') to each other if they contain the same values. Let’s take a look at the updated example, using a ''%%record%%'' type for ''%%UserId%%''. Thus, if someone were to pass an ''%%int%%'' to ''%%updateUserRole%%'', it would throw an error: @@ -56,25 +56,25 @@ Now, if ''%%$uid%%'' is accidentally set to an integer, the call to ''%%updateUs ===== Proposal ===== -This RFC proposes the introduction of a ''%%record%%'' keyword in PHP to define immutable data objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but can’t extend other records or classes; composition is allowed, however. +This RFC proposes the introduction of a ''%%record%%'' keyword in PHP to define immutable value objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but can’t extend other records or classes; composition is allowed, however. ==== Syntax and semantics ==== === Definition === -A **record** is defined by the keyword ''%%record%%'', followed by the name of its type (e.g., ''%%UserId%%''), and then must list one or more typed parameters (e.g., ''%%int $id%%'') that become properties of the record. A parameter may provide ''%%private%%'' or ''%%public%%'' modifiers, but are ''%%public%%'' by default. This is referred to as the "inline constructor." +A **record** is defined by the keyword ''%%record%%'', followed by the name of its type (e.g., ''%%UserId%%''), and then must list one or more typed parameters (e.g., ''%%int $id%%'') that become properties of the record. A parameter may provide ''%%private%%'' or ''%%public%%'' modifiers, but are ''%%public%%'' by when not specified. This is referred to as the "inline constructor." A **record** may optionally implement an interface using the ''%%implements%%'' keyword, which may optionally be followed by a record body enclosed in curly braces ''%%{}%%''. A **record** may not extend another record or class. -A **record** may contain a traditional constructor with zero arguments to perform further initialization, but if it does, it must take zero arguments. +A **record** may contain a traditional constructor with zero arguments to perform further initialization. A **record** body may contain property hooks, methods, and use traits. A **record** body may also declare properties whose values are only mutable during a constructor call. At any other time, the property is immutable. -A **record** body may also contain static methods and properties, which behave identically to class static methods and properties. They may be accessed using the ''%%::%%'' operator. +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. namespace Paint; @@ -125,13 +125,13 @@ record PaintBucket(StockPaint ...$constituents) { === Usage === -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. +A record may be used as a readonly class, as the behavior of the two is very similar once instantiated, assisting in migrating from one implementation to another. === Optional parameters and default values === A ''%%record%%'' can also be defined with optional parameters that are set if omitted during instantiation. -One or more properties defined in the inline constructor may have a default value declared using the same syntax and rules as any other default parameter declared in methods/functions. If a property has a default value, it is optional when instantiating the record and PHP will assign the default value to the property. +One or more properties defined in the inline constructor may have a default value declared using the same syntax and rules as any other default parameter declared in methods/functions. If a property has a default value, it is optional when instantiating the record, and PHP will assign the default value to the property. record Rectangle(int $x, int $y = 10); @@ -140,48 +140,59 @@ var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 === Auto-generated with method === -To enhance the usability of records, the RFC proposes automatically generating a ''%%with%%'' method for each record. This method allows for partial updates of properties, creating a new instance of the record with the specified properties updated. +To make records more useful, the RFC proposes generating a ''%%with%%'' method for each record. This method allows for partial updates to the properties, creating a new instance of the record with the specified properties updated. -The auto-generated ''%%with%%'' method accepts only named arguments defined in the constructor, except variadic arguments. For variadic arguments, these don’t require named arguments. No other property names can be used, and it returns a record object with the given values. +== How the with method works == -Example showing how this works with properties: +**Named arguments** + +The ''%%with%%'' method accepts only named arguments defined in the inline constructor. Properties not defined in the inline constructor can’t be updated by this method. + +**Variadic arguments** + +Variadic arguments from the inline constructor don’t require named arguments in the ''%%with%%'' method. However, mixing variadic arguments in the same ''%%with%%'' method call is not allowed by PHP syntax. + +Using named arguments: record UserId(int $id) { public string $serialNumber; - + public function __construct() { $this->serialNumber = "U{$this->id}"; } } $userId = UserId(1); -$otherId = $userId->with(2); // failure due to not using named arguments -$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not defined in the inline constructor -$otherId = $userId->with(id: 2); // success +$otherId = $userId->with(2); // Fails: Named arguments must be used +$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not in the inline constructor +$otherId = $userId->with(id: 2); // Success: id is updated -Example showing how this works with variadic arguments, take note that PHP doesn’t allow sending variadic arguments with named arguments. Thus, the developer may need to perform multiple operations to construct a record from an existing one using variadic arguments. +Using variadic arguments: record Vector(int $dimensions, int ...$values); $vector = Vector(3, 1, 2, 3); -$vector = $vector->with(4, 5, 6); // automatically sent to $values -$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: mixing named arguments with variadic arguments is not allowed by PHP syntax -$vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // set dimensions first, the set values. +$vector = $vector->with(dimensions: 4); // Success: values are updated +$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: Mixing named and variadic arguments +$vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // Success: First update dimensions, then values -This may look a bit confusing at first glance, but PHP currently doesn’t allow mixing named arguments with variadic arguments. - == Custom with method == A developer may define their own ''%%with%%'' method if they so choose, and reference the generated ''%%with%%'' method using ''%%parent::with()%%''. This allows a developer to define policies or constraints on how data is updated. +Contravariance and covariance are enforced in the developer’s code: + + * Contravariance: the parameter type of the custom ''%%with%%'' method must be a supertype of the generated ''%%with%%'' method. + * Covariance: the return type of the custom ''%%with%%'' method must be ''%%self%%'' of the generated ''%%with%%'' method. + record Planet(string $name, int $population) { // create a with method that only accepts population updates - public function with(int $population) { + public function with(int $population): Planet { return parent::with(population: $population); } } @@ -194,9 +205,11 @@ $mickey = $pluto->with(name: "Mickey"); // Error: no named argument for populati === Constructors === -A **record** has two concepts of construction: the inline constructor and the traditional constructor. +A **record** has two types of constructors: the inline constructor and the traditional constructor. -The inline constructor is always required and must define at least one parameter. The traditional constructor is optional and can be used for further initialization logic. +The inline constructor is always required and must define at least one parameter. The traditional constructor is optional and can be used for further initialization logic, but must not accept any arguments. + +When a traditional constructor exists and is called, the properties are already initialized to the value of the inline constructor and are mutable until the end of the method, at which point they become immutable. // Inline constructor @@ -215,8 +228,6 @@ record User(string $name, string $email) { } -When a traditional constructor exists and is called, the properties are already initialized to the value of the inline constructor and are mutable until the end of the method, at which point they become immutable. - ==== Mental models and how it works ==== 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. @@ -228,31 +239,53 @@ record Point(int $x, int $y); // similar to declaring the following function and class -class Point { +// used during construction to allow immutability +class Point_Implementation { public int $x; public int $y; - + public function __construct() {} + + public function with(...$parameters) { + // validity checks omitted for brevity + $parameters = array_merge([$this->x, $this->y], $parameters); + return Point(...$parameters); + } +} + +// used to enforce immutability but has the same implementation +readonly class Point { + public function __construct(public int $x, public int $y) {} + + public function with(...$parameters) { + // validity checks omitted for brevity + $parameters = array_merge([$this->x, $this->y], $parameters); + return Point(...$parameters); + } } function Point(int $x, int $y): Point { static $points = []; - $key = "$x,$y"; + // look up the identity of the point + $key = hash_func($x, $y); if ($points[$key] ?? null) { + // return an existing point return $points[$key]; } - - $reflector = new \ReflectionClass(Point::class); + + // create a new point + $reflector = new \ReflectionClass(Point_Implementation::class); $point = $reflector->newInstanceWithoutConstructor(); $point->x = $x; $point->y = $y; $point->__construct(); - $reflector->finalizeRecord($point); + // copy properties to an immutable point and return it + $point = new Point($point->x, $point->y); return $points[$key] = $point; } -In reality, it isn’t too much different than what actually happens in C, except that the C version is more efficient and frees up memory when the object is no longer referenced. +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. ==== Performance considerations ==== From 57109f5e3327c6b4e9336aae5f5f5a72272061df Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Fri, 2 Aug 2024 13:24:36 +0200 Subject: [PATCH 22/46] refactor reflection and make it make more sense --- drafts/records.md | 150 ++++++++++++++++++++++------------------- published/records.ptxt | 133 ++++++++++++++++++------------------ 2 files changed, 148 insertions(+), 135 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 4ea6068..253e826 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -243,7 +243,7 @@ A **record** has two types of constructors: the inline constructor and the tradi The inline constructor is always required and must define at least one parameter. The traditional constructor is optional and can be used for further initialization logic, -but must not accept any arguments. +but mustn’t accept any arguments. When a traditional constructor exists and is called, the properties are already initialized to the value of the inline constructor @@ -275,7 +275,11 @@ as a factory function that creates a new object or retrieves an existing object For example, this would be a valid mental model for a Point record: ```php -record Point(int $x, int $y); +record Point(int $x, int $y) { + public function add(Point $point): Point { + return Point($this->x + $point->x, $this->y + $point->y); + } +} // similar to declaring the following function and class @@ -291,17 +295,29 @@ class Point_Implementation { $parameters = array_merge([$this->x, $this->y], $parameters); return Point(...$parameters); } + + public function add(Point $point): Point { + return Point($this->x + $point->x, $this->y + $point->y); + } +} + +interface Record { + public function with(...$parameters): self; } // used to enforce immutability but has the same implementation -readonly class Point { +readonly class Point implements Record { public function __construct(public int $x, public int $y) {} - public function with(...$parameters) { + public function with(...$parameters): self { // validity checks omitted for brevity $parameters = array_merge([$this->x, $this->y], $parameters); return Point(...$parameters); } + + public function add(Point $point): Point { + return Point($this->x + $point->x, $this->y + $point->y); + } } function Point(int $x, int $y): Point { @@ -395,99 +411,93 @@ $time2 = Time(5000); echo $time1 < $time2; // Outputs: true ``` +### Type hinting + +A `\Record` interface will be added to the engine to allow type hinting for records. +All records implement this interface. + +```php +function doSomething(\Record $record): void { + // ... +} +``` + +The only method on the interface is `with`, which is a variadic method that accepts named arguments and returns `self`. + ### Reflection -Records can be interacted with via ReflectionClass, similar to readonly classes. -For instance, a developer can inspect private properties but not change them, as records are immutable. +A new reflection class will be added to support records: +`ReflectionRecord` which will inherit from `ReflectionClass` and add a few additional methods: -Developers may create new instances of records using ReflectionFunction or ReflectionClass. More on this below. +- `ReflectionRecord::finalizeRecord(object $instance): Record`: Finalizes a record under construction, making it immutable. +- `ReflectionRecord::isRecord(mixed $object): bool`: Returns `true` if the object is a record, and `false` otherwise. +- `ReflectionRecord::getInlineConstructor(): ReflectionFunction`: Returns the inline constructor of the record as `ReflectionFunction`. +- `ReflectionRecord::getTraditionalConstructor(): ReflectionMethod`: Returns the traditional constructor of the record as `ReflectionMethod`. +- `ReflectionRecord::makeMutable(Record $instance): object`: Returns a new record instance with the properties mutable. +- `ReflectionRecord::isMutable(Record $instance): bool`: Returns `true` if the record is mutable, and `false` otherwise. -#### ReflectionClass support +Using `ReflectionRecord` will allow developers to inspect records, their properties, and methods, +as well as create new instances for testing or custom deserialization. -It can be used to inspect records, their properties, and methods. -Any attempt to modify finalized record properties via reflection will throw a `ReflectionException` exception, -maintaining immutability. +Attempting to use `ReflectionClass` or `ReflectionFunction` on a record will throw a `ReflectionException` exception. -``` php -$point = Point(3, 4); -$reflection = new \ReflectionClass($point); +#### finalizeRecord() -foreach ($reflection->getProperties() as $property) { - echo $property->getName() . ': ' . $property->getValue($point) . PHP_EOL; -} -``` +The `finalizeRecord()` method is used to make a record immutable and look up its value in the internal cache, +returning an instance that represents the finalized record. -#### Immutability enforcement +Calling `finalizeRecord()` on a record that has already been finalized will return the same instance. -Attempts to modify record properties via reflection will throw a `ReflectionException` exception. +#### isRecord() -``` php -try { - $property = $reflection->getProperty('x'); - $property->setValue($point, 10); // This will throw an exception -} catch (\ReflectionException $e) { - echo 'Exception: ' . $e->getMessage() . PHP_EOL; // "Cannot modify a record property" -} -``` +The `isRecord()` method is used to determine if an object is a record. It returns `true` if the object is a record, -#### ReflectionFunction for inline constructor +#### getInlineConstructor() -Using `ReflectionFunction` on a record will reflect the inline constructor and can be used to construct new instances. +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. -``` php -$constructor = new \ReflectionFunction('Geometry\Point'); -echo 'Constructor Parameters: '; -foreach ($constructor->getParameters() as $param) { - echo $param->getName() . ' '; -} -``` +#### getTraditionalConstructor() -#### Bypassing constructors +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. -During custom deserialization, a developer may need to bypass constructors to fill in properties. -Since records are mutable until the constructor is called, -this can be done by setting the properties before calling `finalizeRecord()`. +#### makeMutable() -Note that until `finalizeRecord()` is called, any guarantees of immutability and value semantics are **not enforced**. +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. -```php -record Point(int $x, int $y); - -$example = Point(1, 2); - -$pointReflector = new \ReflectionClass(Point::class); -// create a new point while keeping the properties mutable. -$point = $pointReflector->newInstanceWithoutConstructor(); -$point->x = 1; -$point->y = 2; -assert($example !== $point); // true -$pointReflector->finalizeRecord($point); -assert($example === $point); // true -``` +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`. -Alternatively, a developer can use `ReflectionFunction` to get access to the inline constructor and call it directly: +#### isMutable() -```php -record Point(int $x, int $y); +The `isMutable()` method is used +to determine if a record has been made mutable via `makeMutable()` or otherwise not yet finalized. -$example = Point(1, 2); +#### Custom deserialization example -$pointReflector = new \ReflectionFunction(Point::class); -$point = $pointReflector->invoke(1, 2); +In cases where custom deserialization is required, +a developer can use `ReflectionRecord` to manually construct a new instance of a record. -assert($example === $point); // true -``` +```php +record Seconds(int $seconds); -#### New and/or modified functions and methods +$example = Seconds(5); -- Calling `is_object($record)` will return `true`. -- A new function, `is_record($record)`, will return `true` for records, and `false` otherwise. -- Calling `get_class($record)` will return the record name as a string. -- A new method, `ReflectionClass::finalizeRecord($instance)`, will be added to finalize a record, making it immutable. +$reflector = new ReflectionRecord(ExpirationDate::class); +$expiration = $reflector->newInstanceWithoutConstructor(); +$expiration->seconds = 5; +assert($example !== $expiration); // true +$expiration = $reflector->finalizeRecord($expiration); +assert($example === $expiration); // true +``` ### var_dump -When passed an instance of a record the `var_dump()` function will generate output the same +When passed an instance of a record the `var_dump()` function will output the same as if an equivalent object were passed — e.g., both having the same properties — except the output generated will replace the prefix text "object" with the text "record." diff --git a/published/records.ptxt b/published/records.ptxt index de9a3b6..7c03f02 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -207,7 +207,7 @@ $mickey = $pluto->with(name: "Mickey"); // Error: no named argument for populati A **record** has two types of constructors: the inline constructor and the traditional constructor. -The inline constructor is always required and must define at least one parameter. The traditional constructor is optional and can be used for further initialization logic, but must not accept any arguments. +The inline constructor is always required and must define at least one parameter. The traditional constructor is optional and can be used for further initialization logic, but mustn’t accept any arguments. When a traditional constructor exists and is called, the properties are already initialized to the value of the inline constructor and are mutable until the end of the method, at which point they become immutable. @@ -235,7 +235,11 @@ From the perspective of a developer, declaring a record declares an object and f For example, this would be a valid mental model for a Point record: -record Point(int $x, int $y); +record Point(int $x, int $y) { + public function add(Point $point): Point { + return Point($this->x + $point->x, $this->y + $point->y); + } +} // similar to declaring the following function and class @@ -251,17 +255,29 @@ class Point_Implementation { $parameters = array_merge([$this->x, $this->y], $parameters); return Point(...$parameters); } + + public function add(Point $point): Point { + return Point($this->x + $point->x, $this->y + $point->y); + } +} + +interface Record { + public function with(...$parameters): self; } // used to enforce immutability but has the same implementation -readonly class Point { +readonly class Point implements Record { public function __construct(public int $x, public int $y) {} - public function with(...$parameters) { + public function with(...$parameters): self { // validity checks omitted for brevity $parameters = array_merge([$this->x, $this->y], $parameters); return Point(...$parameters); } + + public function add(Point $point): Point { + return Point($this->x + $point->x, $this->y + $point->y); + } } function Point(int $x, int $y): Point { @@ -344,94 +360,81 @@ $time2 = Time(5000); echo $time1 < $time2; // Outputs: true +==== Type hinting ==== + +A ''%%\Record%%'' interface will be added to the engine to allow type hinting for records. All records implement this interface. + + +function doSomething(\Record $record): void { + // ... +} + + +The only method on the interface is ''%%with%%'', which is a variadic method that accepts named arguments and returns ''%%self%%''. + ==== Reflection ==== -Records can be interacted with via ReflectionClass, similar to readonly classes. For instance, a developer can inspect private properties but not change them, as records are immutable. +A new reflection class will be added to support records: ''%%ReflectionRecord%%'' which will inherit from ''%%ReflectionClass%%'' and add a few additional methods: -Developers may create new instances of records using ReflectionFunction or ReflectionClass. More on this below. + * ''%%ReflectionRecord::finalizeRecord(object $instance): Record%%'': Finalizes a record under construction, making it immutable. + * ''%%ReflectionRecord::isRecord(mixed $object): bool%%'': Returns ''%%true%%'' if the object is a record, and ''%%false%%'' otherwise. + * ''%%ReflectionRecord::getInlineConstructor(): ReflectionFunction%%'': Returns the inline constructor of the record as ''%%ReflectionFunction%%''. + * ''%%ReflectionRecord::getTraditionalConstructor(): ReflectionMethod%%'': Returns the traditional constructor of the record as ''%%ReflectionMethod%%''. + * ''%%ReflectionRecord::makeMutable(Record $instance): object%%'': Returns a new record instance with the properties mutable. + * ''%%ReflectionRecord::isMutable(Record $instance): bool%%'': Returns ''%%true%%'' if the record is mutable, and ''%%false%%'' otherwise. -=== ReflectionClass support === +Using ''%%ReflectionRecord%%'' will allow developers to inspect records, their properties, and methods, as well as create new instances for testing or custom deserialization. -It can be used to inspect records, their properties, and methods. Any attempt to modify finalized record properties via reflection will throw a ''%%ReflectionException%%'' exception, maintaining immutability. +Attempting to use ''%%ReflectionClass%%'' or ''%%ReflectionFunction%%'' on a record will throw a ''%%ReflectionException%%'' exception. - -$point = Point(3, 4); -$reflection = new \ReflectionClass($point); +=== finalizeRecord() === -foreach ($reflection->getProperties() as $property) { - echo $property->getName() . ': ' . $property->getValue($point) . PHP_EOL; -} - +The ''%%finalizeRecord()%%'' method is used to make a record immutable and look up its value in the internal cache, returning an instance that represents the finalized record. -=== Immutability enforcement === +Calling ''%%finalizeRecord()%%'' on a record that has already been finalized will return the same instance. -Attempts to modify record properties via reflection will throw a ''%%ReflectionException%%'' exception. +=== isRecord() === - -try { - $property = $reflection->getProperty('x'); - $property->setValue($point, 10); // This will throw an exception -} catch (\ReflectionException $e) { - echo 'Exception: ' . $e->getMessage() . PHP_EOL; // "Cannot modify a record property" -} - +The ''%%isRecord()%%'' method is used to determine if an object is a record. It returns ''%%true%%'' if the object is a record, -=== ReflectionFunction for inline constructor === +=== getInlineConstructor() === -Using ''%%ReflectionFunction%%'' on a record will reflect the inline constructor and can be used to construct new instances. +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. - -$constructor = new \ReflectionFunction('Geometry\Point'); -echo 'Constructor Parameters: '; -foreach ($constructor->getParameters() as $param) { - echo $param->getName() . ' '; -} - +=== getTraditionalConstructor() === -=== Bypassing constructors === +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. -During custom deserialization, a developer may need to bypass constructors to fill in properties. Since records are mutable until the constructor is called, this can be done by setting the properties before calling ''%%finalizeRecord()%%''. +=== makeMutable() === -Note that until ''%%finalizeRecord()%%'' is called, any guarantees of immutability and value semantics are **not enforced**. +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. - -record Point(int $x, int $y); - -$example = Point(1, 2); - -$pointReflector = new \ReflectionClass(Point::class); -// create a new point while keeping the properties mutable. -$point = $pointReflector->newInstanceWithoutConstructor(); -$point->x = 1; -$point->y = 2; -assert($example !== $point); // true -$pointReflector->finalizeRecord($point); -assert($example === $point); // true - +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%%''. -Alternatively, a developer can use ''%%ReflectionFunction%%'' to get access to the inline constructor and call it directly: +=== isMutable() === - -record Point(int $x, int $y); +The ''%%isMutable()%%'' method is used to determine if a record has been made mutable via ''%%makeMutable()%%'' or otherwise not yet finalized. -$example = Point(1, 2); +=== Custom deserialization example === -$pointReflector = new \ReflectionFunction(Point::class); -$point = $pointReflector->invoke(1, 2); +In cases where custom deserialization is required, a developer can use ''%%ReflectionRecord%%'' to manually construct a new instance of a record. -assert($example === $point); // true - + +record Seconds(int $seconds); -=== New and/or modified functions and methods === +$example = Seconds(5); - * Calling ''%%is_object($record)%%'' will return ''%%true%%''. - * A new function, ''%%is_record($record)%%'', will return ''%%true%%'' for records, and ''%%false%%'' otherwise. - * Calling ''%%get_class($record)%%'' will return the record name as a string. - * A new method, ''%%ReflectionClass::finalizeRecord($instance)%%'', will be added to finalize a record, making it immutable. +$reflector = new ReflectionRecord(ExpirationDate::class); +$expiration = $reflector->newInstanceWithoutConstructor(); +$expiration->seconds = 5; +assert($example !== $expiration); // true +$expiration = $reflector->finalizeRecord($expiration); +assert($example === $expiration); // true + ==== var_dump ==== -When passed an instance of a record the ''%%var_dump()%%'' function will generate output the same as if an equivalent object were passed — e.g., both having the same properties — except the output generated will replace the prefix text "object" with the text "record." +When passed an instance of a record the ''%%var_dump()%%'' function will output the same as if an equivalent object were passed — e.g., both having the same properties — except the output generated will replace the prefix text "object" with the text "record." record(Point)#1 (2) { From 1b910372b43682c1e8196e151e9d1d07441c25c1 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Fri, 2 Aug 2024 14:24:54 +0200 Subject: [PATCH 23/46] address feedback --- drafts/records.md | 37 +++++++++++++++++++------------------ published/records.ptxt | 30 +++++++++++++++--------------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 253e826..d7830b2 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -30,9 +30,9 @@ function updateUserRole(int $userId, string $role): void { $user = getUser(/*...*/) $uid = $user->id; // ... -$uid = 5; // somehow accidentally sets uid to an unrelated integer +$uid = 5; // accidentally sets uid to an unrelated integer // ... -updateUserRole($uid, 'admin'); // accidental passing of a nonsensical value for uid +updateUserRole($uid, 'admin'); // accidental passes a nonsensical value for uid ``` Currently, the only solution to this is to use a **class**, but this requires significant boilerplate code. @@ -43,7 +43,7 @@ Further, **readonly classes** have many edge cases and are rather unwieldy. Like arrays, strings, and other values, **record** objects are strongly equal (`===`) to each other if they contain the same values. -Let’s take a look at the updated example, using a `record` type for `UserId`. +Let’s take a look at an updated example using a `record` type for `UserId`. Thus, if someone were to pass an `int` to `updateUserRole`, it would throw an error: ```php @@ -58,7 +58,7 @@ $uid = $user->id; // $uid is a UserId object // ... $uid = 5; // ... -updateUserRole($uid, 'admin'); // This will throw an error +updateUserRole($uid, 'admin'); // This will throw a TypeError ``` Now, if `$uid` is accidentally set to an integer, @@ -69,8 +69,8 @@ because the function expects a `UserId` object instead of a plain integer. This RFC proposes the introduction of a `record` keyword in PHP to define immutable value objects. These objects will allow properties to be initialized concisely -and will provide built-in methods for common operations -such as modifying properties and equality checks using a function-like instantiation syntax. +and will provide built-in methods for common operations such as modifying properties, +performing equality checks, and using a function-like instantiation syntax. Records can implement interfaces and use traits but can’t extend other records or classes; composition is allowed, however. @@ -81,7 +81,7 @@ composition is allowed, however. A **record** is defined by the keyword `record`, followed by the name of its type (e.g., `UserId`), and then must list one or more typed parameters (e.g., `int $id`) that become properties of the record. -A parameter may provide `private` or `public` modifiers, but are `public` by when not specified. +A parameter may provide `private` or `public` modifiers, but are `public` when not specified. This is referred to as the "inline constructor." A **record** may optionally implement an interface using the `implements` keyword, @@ -150,7 +150,7 @@ record PaintBucket(StockPaint ...$constituents) { #### Usage A record may be used as a readonly class, -as the behavior of the two is very similar once instantiated, +as the behavior of the two is very similar, assisting in migrating from one implementation to another. #### Optional parameters and default values @@ -158,9 +158,9 @@ assisting in migrating from one implementation to another. A `record` can also be defined with optional parameters that are set if omitted during instantiation. One or more properties defined in the inline constructor may have a default value -declared using the same syntax and rules as any other default parameter declared in methods/functions. +declared using the same syntax and rules as any other default parameter in methods/functions. If a property has a default value, -it is optional when instantiating the record, and PHP will assign the default value to the property. +it is optional when instantiating the record, and PHP will assign the default value to the property if omitted. ``` php record Rectangle(int $x, int $y = 10); @@ -183,7 +183,7 @@ Properties not defined in the inline constructor can’t be updated by this meth **Variadic arguments** Variadic arguments from the inline constructor don’t require named arguments in the `with` method. -However, mixing variadic arguments in the same `with` method call is not allowed by PHP syntax. +However, mixing named and variadic arguments in the same `with` method call is not allowed by PHP syntax. Using named arguments: @@ -198,7 +198,7 @@ record UserId(int $id) { $userId = UserId(1); $otherId = $userId->with(2); // Fails: Named arguments must be used -$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not in the inline constructor +$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not defined in the inline constructor $otherId = $userId->with(id: 2); // Success: id is updated ``` @@ -209,17 +209,17 @@ record Vector(int $dimensions, int ...$values); $vector = Vector(3, 1, 2, 3); $vector = $vector->with(dimensions: 4); // Success: values are updated -$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: Mixing named and variadic arguments +$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: mixing named arguments with variadic arguments is not allowed by PHP syntax $vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // Success: First update dimensions, then values ``` ##### Custom with method -A developer may define their own `with` method if they so choose, +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. -Contravariance and covariance are enforced in the developer’s code: +Contravariance and covariance are enforced in the developer’s code via the `Record` interface: - Contravariance: the parameter type of the custom `with` method must be a supertype of the generated `with` method. - Covariance: the return type of the custom `with` method must be `self` of the generated `with` method. @@ -243,11 +243,12 @@ A **record** has two types of constructors: the inline constructor and the tradi The inline constructor is always required and must define at least one parameter. The traditional constructor is optional and can be used for further initialization logic, -but mustn’t accept any arguments. +but must not accept any arguments. When a traditional constructor exists and is called, -the properties are already initialized to the value of the inline constructor -and are mutable until the end of the method, at which point they become immutable. +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. ```php // Inline constructor diff --git a/published/records.ptxt b/published/records.ptxt index 7c03f02..f2c5131 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -24,9 +24,9 @@ function updateUserRole(int $userId, string $role): void { $user = getUser(/*...*/) $uid = $user->id; // ... -$uid = 5; // somehow accidentally sets uid to an unrelated integer +$uid = 5; // accidentally sets uid to an unrelated integer // ... -updateUserRole($uid, 'admin'); // accidental passing of a nonsensical value for uid +updateUserRole($uid, 'admin'); // accidental passes a nonsensical value for uid Currently, the only solution to this is to use a **class**, but this requires significant boilerplate code. Further, **readonly classes** have many edge cases and are rather unwieldy. @@ -35,7 +35,7 @@ Currently, the only solution to this is to use a **class**, but this requires si Like arrays, strings, and other values, **record** objects are strongly equal (''%%===%%'') to each other if they contain the same values. -Let’s take a look at the updated example, using a ''%%record%%'' type for ''%%UserId%%''. Thus, if someone were to pass an ''%%int%%'' to ''%%updateUserRole%%'', it would throw an error: +Let’s take a look at an updated example using a ''%%record%%'' type for ''%%UserId%%''. Thus, if someone were to pass an ''%%int%%'' to ''%%updateUserRole%%'', it would throw an error: record UserId(int $id); @@ -49,20 +49,20 @@ $uid = $user->id; // $uid is a UserId object // ... $uid = 5; // ... -updateUserRole($uid, 'admin'); // This will throw an error +updateUserRole($uid, 'admin'); // This will throw a TypeError Now, if ''%%$uid%%'' is accidentally set to an integer, the call to ''%%updateUserRole%%'' will throw a ''%%TypeError%%'' because the function expects a ''%%UserId%%'' object instead of a plain integer. ===== Proposal ===== -This RFC proposes the introduction of a ''%%record%%'' keyword in PHP to define immutable value objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but can’t extend other records or classes; composition is allowed, however. +This RFC proposes the introduction of a ''%%record%%'' keyword in PHP to define immutable value objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties, performing equality checks, and using a function-like instantiation syntax. Records can implement interfaces and use traits but can’t extend other records or classes; composition is allowed, however. ==== Syntax and semantics ==== === Definition === -A **record** is defined by the keyword ''%%record%%'', followed by the name of its type (e.g., ''%%UserId%%''), and then must list one or more typed parameters (e.g., ''%%int $id%%'') that become properties of the record. A parameter may provide ''%%private%%'' or ''%%public%%'' modifiers, but are ''%%public%%'' by when not specified. This is referred to as the "inline constructor." +A **record** is defined by the keyword ''%%record%%'', followed by the name of its type (e.g., ''%%UserId%%''), and then must list one or more typed parameters (e.g., ''%%int $id%%'') that become properties of the record. A parameter may provide ''%%private%%'' or ''%%public%%'' modifiers, but are ''%%public%%'' when not specified. This is referred to as the "inline constructor." A **record** may optionally implement an interface using the ''%%implements%%'' keyword, which may optionally be followed by a record body enclosed in curly braces ''%%{}%%''. @@ -125,13 +125,13 @@ record PaintBucket(StockPaint ...$constituents) { === Usage === -A record may be used as a readonly class, as the behavior of the two is very similar once instantiated, assisting in migrating from one implementation to another. +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. === Optional parameters and default values === A ''%%record%%'' can also be defined with optional parameters that are set if omitted during instantiation. -One or more properties defined in the inline constructor may have a default value declared using the same syntax and rules as any other default parameter declared in methods/functions. If a property has a default value, it is optional when instantiating the record, and PHP will assign the default value to the property. +One or more properties defined in the inline constructor may have a default value declared using the same syntax and rules as any other default parameter in methods/functions. If a property has a default value, it is optional when instantiating the record, and PHP will assign the default value to the property if omitted. record Rectangle(int $x, int $y = 10); @@ -150,7 +150,7 @@ The ''%%with%%'' method accepts only named arguments defined in the inline const **Variadic arguments** -Variadic arguments from the inline constructor don’t require named arguments in the ''%%with%%'' method. However, mixing variadic arguments in the same ''%%with%%'' method call is not allowed by PHP syntax. +Variadic arguments from the inline constructor don’t require named arguments in the ''%%with%%'' method. However, mixing named and variadic arguments in the same ''%%with%%'' method call is not allowed by PHP syntax. Using named arguments: @@ -165,7 +165,7 @@ record UserId(int $id) { $userId = UserId(1); $otherId = $userId->with(2); // Fails: Named arguments must be used -$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not in the inline constructor +$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not defined in the inline constructor $otherId = $userId->with(id: 2); // Success: id is updated @@ -176,15 +176,15 @@ record Vector(int $dimensions, int ...$values); $vector = Vector(3, 1, 2, 3); $vector = $vector->with(dimensions: 4); // Success: values are updated -$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: Mixing named and variadic arguments +$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: mixing named arguments with variadic arguments is not allowed by PHP syntax $vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // Success: First update dimensions, then values == Custom with method == -A developer may define their own ''%%with%%'' method if they so choose, and reference the generated ''%%with%%'' method using ''%%parent::with()%%''. This allows a developer to define policies or constraints on how data is updated. +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. -Contravariance and covariance are enforced in the developer’s code: +Contravariance and covariance are enforced in the developer’s code via the ''%%Record%%'' interface: * Contravariance: the parameter type of the custom ''%%with%%'' method must be a supertype of the generated ''%%with%%'' method. * Covariance: the return type of the custom ''%%with%%'' method must be ''%%self%%'' of the generated ''%%with%%'' method. @@ -207,9 +207,9 @@ $mickey = $pluto->with(name: "Mickey"); // Error: no named argument for populati A **record** has two types of constructors: the inline constructor and the traditional constructor. -The inline constructor is always required and must define at least one parameter. The traditional constructor is optional and can be used for further initialization logic, but mustn’t accept any arguments. +The inline constructor is always required and must define at least one parameter. The traditional constructor is optional and can be used for further initialization logic, but must not accept any arguments. -When a traditional constructor exists and is called, the properties are already initialized to the value of the inline constructor and are mutable until the end of the method, at which point they become immutable. +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. // Inline constructor From 91d287ea44d524f5111c73c07c10ffd71ea6652e Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Fri, 2 Aug 2024 14:27:18 +0200 Subject: [PATCH 24/46] remove trailing space --- drafts/records.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drafts/records.md b/drafts/records.md index d7830b2..a9d3164 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -67,7 +67,7 @@ because the function expects a `UserId` object instead of a plain integer. ## Proposal -This RFC proposes the introduction of a `record` keyword in PHP to define immutable value objects. +This RFC proposes the introduction of a `record` keyword in PHP to define immutable value objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties, performing equality checks, and using a function-like instantiation syntax. From ce74737793c9493b7674e38669ee09a353dda453 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Fri, 2 Aug 2024 20:33:32 +0200 Subject: [PATCH 25/46] add information about non-trivial values --- drafts/records.md | 39 +++++++++++++++++++++++++++++++++++++++ published/records.ptxt | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/drafts/records.md b/drafts/records.md index a9d3164..94e96af 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -412,6 +412,43 @@ $time2 = Time(5000); echo $time1 < $time2; // Outputs: true ``` +#### Non-trivial values + +For non-trivial values (e.g., objects, closures, resources, etc.), +the `===` operator will return `true` if the two operands reference the same object. + +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: + +```php +$date1 = DateTime('2024-07-19'); +$date2 = DateTime('2024-07-19'); + +record Date(DateTime $date); + +$dateRecord1 = Date($date1); +$dateRecord2 = Date($date2); + +echo $dateRecord1 === $dateRecord2; // Outputs: false +``` + +However, this can be worked around by being a bit creative (see: mental model): + +```php +record Date(string $date) { + public DateTime $datetime; + + public function __construct() { + $this->datetime = new DateTime($this->date); + } +} + +$date1 = Date('2024-07-19'); +$date2 = Date('2024-07-19'); + +echo $date1->datetime === $date2->datetime; // Outputs: true +``` + ### Type hinting A `\Record` interface will be added to the engine to allow type hinting for records. @@ -572,6 +609,8 @@ None. ## Future Scope +- Records for "record-like" types, such as DateTime, DateInterval, and others. + ## Proposed Voting Choices Include these so readers know where you’re heading and can discuss the diff --git a/published/records.ptxt b/published/records.ptxt index f2c5131..30bc8e5 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -360,6 +360,41 @@ $time2 = Time(5000); echo $time1 < $time2; // Outputs: true +=== Non-trivial values === + +For non-trivial values (e.g., objects, closures, resources, etc.), the ''%%===%%'' operator will return ''%%true%%'' if the two operands reference the same object. + +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: + + +$date1 = DateTime('2024-07-19'); +$date2 = DateTime('2024-07-19'); + +record Date(DateTime $date); + +$dateRecord1 = Date($date1); +$dateRecord2 = Date($date2); + +echo $dateRecord1 === $dateRecord2; // Outputs: false + + +However, this can be worked around by being a bit creative (see: mental model): + + +record Date(string $date) { + public DateTime $datetime; + + public function __construct() { + $this->datetime = new DateTime($this->date); + } +} + +$date1 = Date('2024-07-19'); +$date2 = Date('2024-07-19'); + +echo $date1->datetime === $date2->datetime; // Outputs: true + + ==== Type hinting ==== A ''%%\Record%%'' interface will be added to the engine to allow type hinting for records. All records implement this interface. @@ -497,6 +532,8 @@ None. ===== Future Scope ===== + * Records for "record-like" types, such as DateTime, DateInterval, and others. + ===== Proposed Voting Choices ===== Include these so readers know where you’re heading and can discuss the proposed voting options. From 30d19d943ac388365eae493b85e40f42a25d0f3a Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Fri, 2 Aug 2024 20:35:37 +0200 Subject: [PATCH 26/46] make comparisons undefined --- drafts/records.md | 20 +------------------- published/records.ptxt | 20 +------------------- 2 files changed, 2 insertions(+), 38 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 94e96af..590386a 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -392,25 +392,7 @@ A `record` is always strongly equal (`===`) to another record with the same valu 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`. -Comparison operations will behave exactly like they do for classes, for example: - -```php -record Time(float $milliseconds = 0) { - public float $totalSeconds { - get => $this->milliseconds / 1000, - } - - public float $totalMinutes { - get => $this->totalSeconds / 60, - } - /* ... */ -} - -$time1 = Time(1000); -$time2 = Time(5000); - -echo $time1 < $time2; // Outputs: true -``` +Comparison operations will behave exactly like they do for classes, which is currently undefined. #### Non-trivial values diff --git a/published/records.ptxt b/published/records.ptxt index 30bc8e5..8fd50d7 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -340,25 +340,7 @@ echo unserialize($multiple) === Multiple('value1', 'value2'); // Outputs: true 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%%''. -Comparison operations will behave exactly like they do for classes, for example: - - -record Time(float $milliseconds = 0) { - public float $totalSeconds { - get => $this->milliseconds / 1000, - } - - public float $totalMinutes { - get => $this->totalSeconds / 60, - } - /* ... */ -} - -$time1 = Time(1000); -$time2 = Time(5000); - -echo $time1 < $time2; // Outputs: true - +Comparison operations will behave exactly like they do for classes, which is currently undefined. === Non-trivial values === From c1d0a8a1b3a51988771a5cbbef6a4de689c39765 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 15 Aug 2024 16:34:34 +0200 Subject: [PATCH 27/46] add function autoloading --- drafts/function-autoloading.md | 120 ++++++++++++++++++++++++++++ published/function-autoloading.ptxt | 111 +++++++++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 drafts/function-autoloading.md create mode 100644 published/function-autoloading.ptxt diff --git a/drafts/function-autoloading.md b/drafts/function-autoloading.md new file mode 100644 index 0000000..57be0aa --- /dev/null +++ b/drafts/function-autoloading.md @@ -0,0 +1,120 @@ +# PHP RFC: Function Autoloading v3 + +* Version: 0.9 +* Date: 2013-02-24 (use today's date here) +* Author: Robert Landers, landers.robert@gmail.com +* Status: Draft (or Under Discussion or Accepted or Declined) +* First Published at: + +## Introduction + +The topic of supporting function autoloading was brought up many times in the past, this RFC introduces a potential +implementation which would be consistent with what we have for autoloading classes. + +## Proposal + +The suggested change would be pretty straightforward and backwards-compatible: + +1. Add two new constants to spl: SPL_AUTOLOAD_CLASS, SPL_AUTOLOAD_FUNCTION. +2. Add a fourth optional parameter for spl_autoload_register, with a default value of SPL_AUTOLOAD_CLASS. +3. The type for the missing token should also be passed to the $autoload_function callback as a second param. (e.g., + SPL_AUTOLOAD_CLASS for classes, SPL_AUTOLOAD_FUNCTION for functions) + 1. This is necessary to be able to handle multiple types of tokens with a common callback. +4. Change the current class autoloading to only call the autoloaders which match with the SPL_AUTOLOAD_CLASS types. +5. Add the function autoloading to only call the autoloaders which match with the SPL_AUTOLOAD_FUNCTION types. + +There won’t be any changes to the current autoloading mechanism when it comes to classes. +However, if a function + +1. is called in a fully qualified form (e.g., a `use` statement or `\` prefix is used) +2. is not defined +3. and an autoloader is registered with the SPL_AUTOLOAD_FUNCTION type + +then the autoloader will be called with the function name as the first parameter (with the initial slash removed) and +SPL_AUTOLOAD_FUNCTION as the second parameter. + +However, if a function + +1. is called in an unqualified form (e.g., `strlen()`) +2. is not defined locally or globally + +then the autoloader will be called exactly once. + +This prevents unnecessary calls from being made to the autoloader, +as PHP searches in the "local" scope and the "global" scope. +If the function is _still_ not defined after this search, +then the autoloader will be called with the current namespace prepended to the function name. + +Example `PSR-4-style` (one function per file) function autoloader: + +```php + + * Yes + * No + + +## Patches and Tests + +Not yet. + +## Implementation + +After the project is implemented, this section should contain - the +version(s) it was merged into - a link to the git commit(s) - a link to +the PHP manual entry for the feature - a link to the language +specification section (if any) + +## References + + +- [autofunc](https://wiki.php.net/rfc/autofunc): This heavily influenced this RFC. (declined in 2011) +- [function_autoloading](https://wiki.php.net/rfc/function_autoloading): This RFC was declined in 2011. +- [function_autoloading_v2](https://wiki.php.net/rfc/function_autoloading2): This RFC was declined in 2012. + +Thank you for all of those that contributed to the discussions back then. I hope that this RFC will be successful. + +## Rejected Features + +Keep this updated with features that were discussed on the mail lists. diff --git a/published/function-autoloading.ptxt b/published/function-autoloading.ptxt new file mode 100644 index 0000000..14e2f6a --- /dev/null +++ b/published/function-autoloading.ptxt @@ -0,0 +1,111 @@ +====== PHP RFC: Function Autoloading v3 ====== + + * Version: 0.9 + * Date: 2013-02-24 (use today's date here) + * Author: Robert Landers, + * Status: Draft (or Under Discussion or Accepted or Declined) + * First Published at: http://wiki.php.net/rfc/your_rfc_name + +===== Introduction ===== + +The topic of supporting function autoloading was brought up many times in the past, this RFC introduces a potential implementation which would be consistent with what we have for autoloading classes. + +===== Proposal ===== + +The suggested change would be pretty straightforward and backwards-compatible: + + - Add two new constants to spl: SPL_AUTOLOAD_CLASS, SPL_AUTOLOAD_FUNCTION. + - Add a fourth optional parameter for spl_autoload_register, with a default value of SPL_AUTOLOAD_CLASS. + - The type for the missing token should also be passed to the $autoload_function callback as a second param. (e.g., SPL_AUTOLOAD_CLASS for classes, SPL_AUTOLOAD_FUNCTION for functions) + - This is necessary to be able to handle multiple types of tokens with a common callback. + - Change the current class autoloading to only call the autoloaders which match with the SPL_AUTOLOAD_CLASS types. + - Add the function autoloading to only call the autoloaders which match with the SPL_AUTOLOAD_FUNCTION types. + +There won’t be any changes to the current autoloading mechanism when it comes to classes. However, if a function + + - is called in a fully qualified form (e.g., a ''%%use%%'' statement or ''%%\%%'' prefix is used) + - is not defined + - and an autoloader is registered with the SPL_AUTOLOAD_FUNCTION type + +then the autoloader will be called with the function name as the first parameter (with the initial slash removed) and SPL_AUTOLOAD_FUNCTION as the second parameter. + +However, if a function + + - is called in an unqualified form (e.g., ''%%strlen()%%'') + - is not defined locally or globally + +then the autoloader will be called exactly once. + +This prevents unnecessary calls from being made to the autoloader, as PHP searches in the "local" scope and the "global" scope. If the function is //still// not defined after this search, then the autoloader will be called with the current namespace prepended to the function name. + +Example ''%%PSR-4-style%%'' (one function per file) function autoloader: + + + + +===== Backward Incompatible Changes ===== + +There are no backward incompatible changes. + +===== Proposed PHP Version(s) ===== + +8.5 or later. + +===== RFC Impact ===== + +==== To Opcache ==== + +To be determined. + +==== New Constants ==== + +Two new constants will be added to the SPL extension: SPL_AUTOLOAD_CLASS, SPL_AUTOLOAD_FUNCTION. + +===== Open Issues ===== + +To be determined. + +===== Future Scope ===== + +Potentially, constants and stream wrappers can be added in a similar fashion. + +===== Proposed Voting Choices ===== + + + + * Yes + * No + + + +===== Patches and Tests ===== + +Not yet. + +===== Implementation ===== + +After the project is implemented, this section should contain - the version(s) it was merged into - a link to the git commit(s) - a link to the PHP manual entry for the feature - a link to the language specification section (if any) + +===== References ===== + + * [[https://wiki.php.net/rfc/autofunc|autofunc]]: This heavily influenced this RFC. (declined in 2011) + * [[https://wiki.php.net/rfc/function_autoloading|function_autoloading]]: This RFC was declined in 2011. + * [[https://wiki.php.net/rfc/function_autoloading2|function_autoloading_v2]]: This RFC was declined in 2012. + +Thank you for all of those that contributed to the discussions back then. I hope that this RFC will be successful. + +===== Rejected Features ===== + +Keep this updated with features that were discussed on the mail lists. From a32063453710aae539b9090b4c5c9ebdd442c676 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 15 Aug 2024 16:42:00 +0200 Subject: [PATCH 28/46] reword things --- drafts/function-autoloading.md | 18 +++++++----------- published/function-autoloading.ptxt | 16 +++++++--------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/drafts/function-autoloading.md b/drafts/function-autoloading.md index 57be0aa..cd22a33 100644 --- a/drafts/function-autoloading.md +++ b/drafts/function-autoloading.md @@ -1,10 +1,10 @@ -# PHP RFC: Function Autoloading v3 +# PHP RFC: Function Autoloading v4 -* Version: 0.9 -* Date: 2013-02-24 (use today's date here) +* Version: 1.0 +* Date: 2024-08-15 * Author: Robert Landers, landers.robert@gmail.com -* Status: Draft (or Under Discussion or Accepted or Declined) -* First Published at: +* Status: Under Discussion (or Accepted or Declined) +* First Published at: ## Introduction @@ -19,7 +19,6 @@ The suggested change would be pretty straightforward and backwards-compatible: 2. Add a fourth optional parameter for spl_autoload_register, with a default value of SPL_AUTOLOAD_CLASS. 3. The type for the missing token should also be passed to the $autoload_function callback as a second param. (e.g., SPL_AUTOLOAD_CLASS for classes, SPL_AUTOLOAD_FUNCTION for functions) - 1. This is necessary to be able to handle multiple types of tokens with a common callback. 4. Change the current class autoloading to only call the autoloaders which match with the SPL_AUTOLOAD_CLASS types. 5. Add the function autoloading to only call the autoloaders which match with the SPL_AUTOLOAD_FUNCTION types. @@ -37,13 +36,10 @@ However, if a function 1. is called in an unqualified form (e.g., `strlen()`) 2. is not defined locally or globally +3. and an autoloader is registered with the SPL_AUTOLOAD_FUNCTION type -then the autoloader will be called exactly once. - -This prevents unnecessary calls from being made to the autoloader, -as PHP searches in the "local" scope and the "global" scope. -If the function is _still_ not defined after this search, then the autoloader will be called with the current namespace prepended to the function name. +If the autoloader chooses to look up the "basename" of the function, it may do so. Example `PSR-4-style` (one function per file) function autoloader: diff --git a/published/function-autoloading.ptxt b/published/function-autoloading.ptxt index 14e2f6a..5aff3a0 100644 --- a/published/function-autoloading.ptxt +++ b/published/function-autoloading.ptxt @@ -1,10 +1,10 @@ -====== PHP RFC: Function Autoloading v3 ====== +====== PHP RFC: Function Autoloading v4 ====== - * Version: 0.9 - * Date: 2013-02-24 (use today's date here) + * Version: 1.0 + * Date: 2024-08-15 * Author: Robert Landers, - * Status: Draft (or Under Discussion or Accepted or Declined) - * First Published at: http://wiki.php.net/rfc/your_rfc_name + * Status: Under Discussion (or Accepted or Declined) + * First Published at: http://wiki.php.net/rfc/function_autoloading4 ===== Introduction ===== @@ -17,7 +17,6 @@ The suggested change would be pretty straightforward and backwards-compatible: - Add two new constants to spl: SPL_AUTOLOAD_CLASS, SPL_AUTOLOAD_FUNCTION. - Add a fourth optional parameter for spl_autoload_register, with a default value of SPL_AUTOLOAD_CLASS. - The type for the missing token should also be passed to the $autoload_function callback as a second param. (e.g., SPL_AUTOLOAD_CLASS for classes, SPL_AUTOLOAD_FUNCTION for functions) - - This is necessary to be able to handle multiple types of tokens with a common callback. - Change the current class autoloading to only call the autoloaders which match with the SPL_AUTOLOAD_CLASS types. - Add the function autoloading to only call the autoloaders which match with the SPL_AUTOLOAD_FUNCTION types. @@ -33,10 +32,9 @@ However, if a function - is called in an unqualified form (e.g., ''%%strlen()%%'') - is not defined locally or globally + - and an autoloader is registered with the SPL_AUTOLOAD_FUNCTION type -then the autoloader will be called exactly once. - -This prevents unnecessary calls from being made to the autoloader, as PHP searches in the "local" scope and the "global" scope. If the function is //still// not defined after this search, then the autoloader will be called with the current namespace prepended to the function name. +then the autoloader will be called with the current namespace prepended to the function name. If the autoloader chooses to look up the "basename" of the function, it may do so. Example ''%%PSR-4-style%%'' (one function per file) function autoloader: From 908d4826df76c7686188582db797d6efd7a5fc14 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 15 Aug 2024 16:59:46 +0200 Subject: [PATCH 29/46] update example --- drafts/function-autoloading.md | 8 ++++---- published/function-autoloading.ptxt | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/drafts/function-autoloading.md b/drafts/function-autoloading.md index cd22a33..b62e783 100644 --- a/drafts/function-autoloading.md +++ b/drafts/function-autoloading.md @@ -41,18 +41,18 @@ However, if a function then the autoloader will be called with the current namespace prepended to the function name. If the autoloader chooses to look up the "basename" of the function, it may do so. -Example `PSR-4-style` (one function per file) function autoloader: +Example "`PSR-4-style`" (except the last part of the namespace is the file it is in) function autoloader: ```php Date: Thu, 15 Aug 2024 23:24:15 +0200 Subject: [PATCH 30/46] clarify more --- drafts/function-autoloading.md | 53 ++++++++++++++++++++++++++--- published/function-autoloading.ptxt | 43 ++++++++++++++++++++--- 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/drafts/function-autoloading.md b/drafts/function-autoloading.md index b62e783..d1a4597 100644 --- a/drafts/function-autoloading.md +++ b/drafts/function-autoloading.md @@ -13,6 +13,16 @@ implementation which would be consistent with what we have for autoloading class ## Proposal +Before getting into the details, +there are a few terms worth acknowledging so that the proposal can be easily discussed without getting confused: + +1. **Defined function**: A function that the engine has knowledge of, such as in a previously included/required file. +2. **Undefined function**: A function that the engine does not have knowledge of. +3. **Function autoloading**: The process of loading a function that is not defined. +4. **Written function**: A function that is defined in a file. +5. **Local scope**: The current namespace +6. **Global scope**: The global namespace (`\`) + The suggested change would be pretty straightforward and backwards-compatible: 1. Add two new constants to spl: SPL_AUTOLOAD_CLASS, SPL_AUTOLOAD_FUNCTION. @@ -25,8 +35,8 @@ The suggested change would be pretty straightforward and backwards-compatible: There won’t be any changes to the current autoloading mechanism when it comes to classes. However, if a function -1. is called in a fully qualified form (e.g., a `use` statement or `\` prefix is used) -2. is not defined +1. is called in a fully qualified form (e.g., a `use` statement or `\` prefix is used), +2. is not defined, 3. and an autoloader is registered with the SPL_AUTOLOAD_FUNCTION type then the autoloader will be called with the function name as the first parameter (with the initial slash removed) and @@ -34,12 +44,19 @@ SPL_AUTOLOAD_FUNCTION as the second parameter. However, if a function -1. is called in an unqualified form (e.g., `strlen()`) -2. is not defined locally or globally +1. is called in an unqualified form (e.g., `strlen()`), +2. is not defined locally 3. and an autoloader is registered with the SPL_AUTOLOAD_FUNCTION type then the autoloader will be called with the current namespace prepended to the function name. If the autoloader chooses to look up the "basename" of the function, it may do so. +If the function is still undefined in the local scope, +then it will fall back to the global scope—unless the local scope is the global scope. +The function autoloader will not be called again. + +This provides an opportunity +for an autoloader to check for the existence of a function in the local scope and define it, +as well as defer to the global scope if it is not defined. Example "`PSR-4-style`" (except the last part of the namespace is the file it is in) function autoloader: @@ -58,6 +75,34 @@ spl_autoload_register(function ($function, $type) { }, false, false, SPL_AUTOLOAD_FUNCTION); ``` +Performance-wise, this should have minimal impact on existing codebases as there is no default function autoloader. + +For codebases that want to take advantage of function autoloading, +it may be desirable to stick with FQNs for functions and/or employ caches and other techniques where possible. + +### spl_autoload + +The `spl_autoload` function will not be updated. +For backwards compatibility, +there will be no changes to class autoloading and there will not be a default function autoloader. + +### spl_autoload_call + +The `spl_autoload_call` function will be modified to accept a second parameter of one, +(but not both) of the new constants, +with the default value set to SPL_AUTOLOAD_CLASS. +The name of the first parameter will be changed to `$name` to reflect that it can be a class or function name. + +```php +spl_autoload_call('\Some\func', SPL_AUTOLOAD_FUNCTION); // Calls the function autoloader +spl_autoload_call('\Some\func'); // Calls the class autoloader +spl_autoload_call('Some\func', SPL_AUTOLOAD_CLASS); // Calls the class autoloader +spl_autoload_call('Some\func'); // Calls the class autoloader +spl_autoload_call('func', SPL_AUTOLOAD_FUNCTION | SPL_AUTOLOAD_CLASS); // Error: Cannot autoload multiple types +``` + +If the user wants to call multiple autoloaders, they can do so manually. + ## Backward Incompatible Changes There are no backward incompatible changes. diff --git a/published/function-autoloading.ptxt b/published/function-autoloading.ptxt index 3de612b..0e9e460 100644 --- a/published/function-autoloading.ptxt +++ b/published/function-autoloading.ptxt @@ -12,6 +12,15 @@ The topic of supporting function autoloading was brought up many times in the pa ===== Proposal ===== +Before getting into the details, there are a few terms worth acknowledging so that the proposal can be easily discussed without getting confused: + + - **Defined function**: A function that the engine has knowledge of, such as in a previously included/required file. + - **Undefined function**: A function that the engine does not have knowledge of. + - **Function autoloading**: The process of loading a function that is not defined. + - **Written function**: A function that is defined in a file. + - **Local scope**: The current namespace + - **Global scope**: The global namespace (''%%\%%'') + The suggested change would be pretty straightforward and backwards-compatible: - Add two new constants to spl: SPL_AUTOLOAD_CLASS, SPL_AUTOLOAD_FUNCTION. @@ -22,19 +31,21 @@ The suggested change would be pretty straightforward and backwards-compatible: There won’t be any changes to the current autoloading mechanism when it comes to classes. However, if a function - - is called in a fully qualified form (e.g., a ''%%use%%'' statement or ''%%\%%'' prefix is used) - - is not defined + - is called in a fully qualified form (e.g., a ''%%use%%'' statement or ''%%\%%'' prefix is used), + - is not defined, - and an autoloader is registered with the SPL_AUTOLOAD_FUNCTION type then the autoloader will be called with the function name as the first parameter (with the initial slash removed) and SPL_AUTOLOAD_FUNCTION as the second parameter. However, if a function - - is called in an unqualified form (e.g., ''%%strlen()%%'') - - is not defined locally or globally + - is called in an unqualified form (e.g., ''%%strlen()%%''), + - is not defined locally - and an autoloader is registered with the SPL_AUTOLOAD_FUNCTION type -then the autoloader will be called with the current namespace prepended to the function name. If the autoloader chooses to look up the "basename" of the function, it may do so. +then the autoloader will be called with the current namespace prepended to the function name. If the autoloader chooses to look up the "basename" of the function, it may do so. If the function is still undefined in the local scope, then it will fall back to the global scope—unless the local scope is the global scope. The function autoloader will not be called again. + +This provides an opportunity for an autoloader to check for the existence of a function in the local scope and define it, as well as defer to the global scope if it is not defined. Example "''%%PSR-4-style%%''" (except the last part of the namespace is the file it is in) function autoloader: @@ -53,6 +64,28 @@ spl_autoload_register(function ($function, $type) { }, false, false, SPL_AUTOLOAD_FUNCTION); +Performance-wise, this should have minimal impact on existing codebases as there is no default function autoloader. + +For codebases that want to take advantage of function autoloading, it may be desirable to stick with FQNs for functions and/or employ caches and other techniques where possible. + +==== spl_autoload ==== + +The ''%%spl_autoload%%'' function will not be updated. For backwards compatibility, there will be no changes to class autoloading and there will not be a default function autoloader. + +==== spl_autoload_call ==== + +The ''%%spl_autoload_call%%'' function will be modified to accept a second parameter of one, (but not both) of the new constants, with the default value set to SPL_AUTOLOAD_CLASS. The name of the first parameter will be changed to ''%%$name%%'' to reflect that it can be a class or function name. + + +spl_autoload_call('\Some\func', SPL_AUTOLOAD_FUNCTION); // Calls the function autoloader +spl_autoload_call('\Some\func'); // Calls the class autoloader +spl_autoload_call('Some\func', SPL_AUTOLOAD_CLASS); // Calls the class autoloader +spl_autoload_call('Some\func'); // Calls the class autoloader +spl_autoload_call('func', SPL_AUTOLOAD_FUNCTION | SPL_AUTOLOAD_CLASS); // Error: Cannot autoload multiple types + + +If the user wants to call multiple autoloaders, they can do so manually. + ===== Backward Incompatible Changes ===== There are no backward incompatible changes. From 72f3856cedced0be4e3a9f7338c39964c9a87c16 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 15 Aug 2024 23:26:43 +0200 Subject: [PATCH 31/46] further clarification --- drafts/function-autoloading.md | 2 +- published/function-autoloading.ptxt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/drafts/function-autoloading.md b/drafts/function-autoloading.md index d1a4597..ea951bc 100644 --- a/drafts/function-autoloading.md +++ b/drafts/function-autoloading.md @@ -19,7 +19,7 @@ there are a few terms worth acknowledging so that the proposal can be easily dis 1. **Defined function**: A function that the engine has knowledge of, such as in a previously included/required file. 2. **Undefined function**: A function that the engine does not have knowledge of. 3. **Function autoloading**: The process of loading a function that is not defined. -4. **Written function**: A function that is defined in a file. +4. **Written function**: A function that exists in a file that the engine may or may not have knowledge of. 5. **Local scope**: The current namespace 6. **Global scope**: The global namespace (`\`) diff --git a/published/function-autoloading.ptxt b/published/function-autoloading.ptxt index 0e9e460..cfee91d 100644 --- a/published/function-autoloading.ptxt +++ b/published/function-autoloading.ptxt @@ -17,7 +17,7 @@ Before getting into the details, there are a few terms worth acknowledging so th - **Defined function**: A function that the engine has knowledge of, such as in a previously included/required file. - **Undefined function**: A function that the engine does not have knowledge of. - **Function autoloading**: The process of loading a function that is not defined. - - **Written function**: A function that is defined in a file. + - **Written function**: A function that exists in a file that the engine may or may not have knowledge of. - **Local scope**: The current namespace - **Global scope**: The global namespace (''%%\%%'') From 1b501212ce0be1fb6b2e8704a799e2cc3b1c1f5b Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Fri, 16 Aug 2024 00:18:11 +0200 Subject: [PATCH 32/46] define function_exists --- drafts/function-autoloading.md | 6 ++++++ published/function-autoloading.ptxt | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/drafts/function-autoloading.md b/drafts/function-autoloading.md index ea951bc..2cc3d07 100644 --- a/drafts/function-autoloading.md +++ b/drafts/function-autoloading.md @@ -93,6 +93,12 @@ The `spl_autoload_call` function will be modified to accept a second parameter o with the default value set to SPL_AUTOLOAD_CLASS. The name of the first parameter will be changed to `$name` to reflect that it can be a class or function name. +### function_exists + +The `function_exists` function will be updated to include a boolean option (`$autoload`) as the second parameter, +which will default to `true`. +If set to `true`, the function autoloader will be called if the function is not defined, otherwise, it will not be called. + ```php spl_autoload_call('\Some\func', SPL_AUTOLOAD_FUNCTION); // Calls the function autoloader spl_autoload_call('\Some\func'); // Calls the class autoloader diff --git a/published/function-autoloading.ptxt b/published/function-autoloading.ptxt index cfee91d..214c61e 100644 --- a/published/function-autoloading.ptxt +++ b/published/function-autoloading.ptxt @@ -76,6 +76,10 @@ The ''%%spl_autoload%%'' function will not be updated. For backwards compatibili The ''%%spl_autoload_call%%'' function will be modified to accept a second parameter of one, (but not both) of the new constants, with the default value set to SPL_AUTOLOAD_CLASS. The name of the first parameter will be changed to ''%%$name%%'' to reflect that it can be a class or function name. +==== function_exists ==== + +The ''%%function_exists%%'' function will be updated to include a boolean option (''%%$autoload%%'') as the second parameter, which will default to ''%%true%%''. If set to ''%%true%%'', the function autoloader will be called if the function is not defined, otherwise, it will not be called. + spl_autoload_call('\Some\func', SPL_AUTOLOAD_FUNCTION); // Calls the function autoloader spl_autoload_call('\Some\func'); // Calls the class autoloader From 1020c04921a9ac595396010e6b5e37d4a6ec7550 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Fri, 16 Aug 2024 09:20:42 +0200 Subject: [PATCH 33/46] fix a typo --- drafts/function-autoloading.md | 13 +++++++------ published/function-autoloading.ptxt | 8 ++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/drafts/function-autoloading.md b/drafts/function-autoloading.md index 2cc3d07..fe95d14 100644 --- a/drafts/function-autoloading.md +++ b/drafts/function-autoloading.md @@ -93,12 +93,6 @@ The `spl_autoload_call` function will be modified to accept a second parameter o with the default value set to SPL_AUTOLOAD_CLASS. The name of the first parameter will be changed to `$name` to reflect that it can be a class or function name. -### function_exists - -The `function_exists` function will be updated to include a boolean option (`$autoload`) as the second parameter, -which will default to `true`. -If set to `true`, the function autoloader will be called if the function is not defined, otherwise, it will not be called. - ```php spl_autoload_call('\Some\func', SPL_AUTOLOAD_FUNCTION); // Calls the function autoloader spl_autoload_call('\Some\func'); // Calls the class autoloader @@ -109,6 +103,13 @@ spl_autoload_call('func', SPL_AUTOLOAD_FUNCTION | SPL_AUTOLOAD_CLASS); // Error: If the user wants to call multiple autoloaders, they can do so manually. + +### function_exists + +The `function_exists` function will be updated to include a boolean option (`$autoload`) as the second parameter, +which will default to `true`. +If set to `true`, the function autoloader will be called if the function is not defined, otherwise, it will not be called. + ## Backward Incompatible Changes There are no backward incompatible changes. diff --git a/published/function-autoloading.ptxt b/published/function-autoloading.ptxt index 214c61e..a5f8bb6 100644 --- a/published/function-autoloading.ptxt +++ b/published/function-autoloading.ptxt @@ -76,10 +76,6 @@ The ''%%spl_autoload%%'' function will not be updated. For backwards compatibili The ''%%spl_autoload_call%%'' function will be modified to accept a second parameter of one, (but not both) of the new constants, with the default value set to SPL_AUTOLOAD_CLASS. The name of the first parameter will be changed to ''%%$name%%'' to reflect that it can be a class or function name. -==== function_exists ==== - -The ''%%function_exists%%'' function will be updated to include a boolean option (''%%$autoload%%'') as the second parameter, which will default to ''%%true%%''. If set to ''%%true%%'', the function autoloader will be called if the function is not defined, otherwise, it will not be called. - spl_autoload_call('\Some\func', SPL_AUTOLOAD_FUNCTION); // Calls the function autoloader spl_autoload_call('\Some\func'); // Calls the class autoloader @@ -90,6 +86,10 @@ spl_autoload_call('func', SPL_AUTOLOAD_FUNCTION | SPL_AUTOLOAD_CLASS); // Error: If the user wants to call multiple autoloaders, they can do so manually. +==== function_exists ==== + +The ''%%function_exists%%'' function will be updated to include a boolean option (''%%$autoload%%'') as the second parameter, which will default to ''%%true%%''. If set to ''%%true%%'', the function autoloader will be called if the function is not defined, otherwise, it will not be called. + ===== Backward Incompatible Changes ===== There are no backward incompatible changes. From 5b3a6c5d049362ab3884d578f25e9bac2bab9d4a Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 18 Aug 2024 10:36:57 +0200 Subject: [PATCH 34/46] add mismatched-arguments --- drafts/function-autoloading.md | 5 ++++- published/function-autoloading.ptxt | 8 +++++--- src/convert-from-md.sh | 3 +++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/drafts/function-autoloading.md b/drafts/function-autoloading.md index fe95d14..3354eff 100644 --- a/drafts/function-autoloading.md +++ b/drafts/function-autoloading.md @@ -112,7 +112,10 @@ If set to `true`, the function autoloader will be called if the function is not ## Backward Incompatible Changes -There are no backward incompatible changes. +### Mismatched arguments + +If an autoloader was registered that can accept more than one argument, +it may fail or perform unexpected behavior when it receives a second argument of `SPL_AUTOLOAD_CLASS`. ## Proposed PHP Version(s) diff --git a/published/function-autoloading.ptxt b/published/function-autoloading.ptxt index a5f8bb6..0741021 100644 --- a/published/function-autoloading.ptxt +++ b/published/function-autoloading.ptxt @@ -92,7 +92,9 @@ The ''%%function_exists%%'' function will be updated to include a boolean option ===== Backward Incompatible Changes ===== -There are no backward incompatible changes. +==== Mismatched arguments ==== + +If an autoloader was registered that can accept more than one argument, it may fail or perform unexpected behavior when it receives a second argument of ''%%SPL_AUTOLOAD_CLASS%%''. ===== Proposed PHP Version(s) ===== @@ -118,13 +120,13 @@ Potentially, constants and stream wrappers can be added in a similar fashion. ===== Proposed Voting Choices ===== - + * Yes * No - + ===== Patches and Tests ===== Not yet. diff --git a/src/convert-from-md.sh b/src/convert-from-md.sh index 8fa25dd..7758da4 100755 --- a/src/convert-from-md.sh +++ b/src/convert-from-md.sh @@ -1,3 +1,6 @@ #!/bin/sh docker run -v "$(pwd)":/data --user "$(id -u)":"$(id -g)" pandoc/latex -t dokuwiki -f gfm "$1" -o "$2" +# remove all and tags +sed -i 's///g' "$2" +sed -i 's/<\/HTML>//g' "$2" From df2e506a6a2bd19b0f9319f8ee4613bfd83334f9 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 18 Aug 2024 10:37:58 +0200 Subject: [PATCH 35/46] always pull --- src/convert-from-md.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/convert-from-md.sh b/src/convert-from-md.sh index 7758da4..bff2601 100755 --- a/src/convert-from-md.sh +++ b/src/convert-from-md.sh @@ -1,6 +1,6 @@ #!/bin/sh -docker run -v "$(pwd)":/data --user "$(id -u)":"$(id -g)" pandoc/latex -t dokuwiki -f gfm "$1" -o "$2" +docker run -v "$(pwd)":/data --pull --user "$(id -u)":"$(id -g)" pandoc/latex -t dokuwiki -f gfm "$1" -o "$2" # remove all and tags sed -i 's///g' "$2" sed -i 's/<\/HTML>//g' "$2" From eb437b9fdd5dd53be2bdc76e3d8f10ebeddf5c9c Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 18 Aug 2024 10:43:12 +0200 Subject: [PATCH 36/46] remove whitespace --- drafts/function-autoloading.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drafts/function-autoloading.md b/drafts/function-autoloading.md index 3354eff..283e4b9 100644 --- a/drafts/function-autoloading.md +++ b/drafts/function-autoloading.md @@ -58,7 +58,7 @@ This provides an opportunity for an autoloader to check for the existence of a function in the local scope and define it, as well as defer to the global scope if it is not defined. -Example "`PSR-4-style`" (except the last part of the namespace is the file it is in) function autoloader: +Example "`PSR-4-style`" (except the last part of the namespace is the file it is in) function autoloader: ```php Date: Sun, 18 Aug 2024 10:43:52 +0200 Subject: [PATCH 37/46] always pull --- src/convert-from-md.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/convert-from-md.sh b/src/convert-from-md.sh index bff2601..d98c6c5 100755 --- a/src/convert-from-md.sh +++ b/src/convert-from-md.sh @@ -1,6 +1,6 @@ #!/bin/sh -docker run -v "$(pwd)":/data --pull --user "$(id -u)":"$(id -g)" pandoc/latex -t dokuwiki -f gfm "$1" -o "$2" +docker run -v "$(pwd)":/data --pull always --user "$(id -u)":"$(id -g)" pandoc/latex -t dokuwiki -f gfm "$1" -o "$2" # remove all and tags sed -i 's///g' "$2" sed -i 's/<\/HTML>//g' "$2" From 16e696720f031048602127e383ca11a73b940711 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 18 Aug 2024 11:33:06 +0200 Subject: [PATCH 38/46] apparently, spl_autoload does need some changes --- drafts/function-autoloading.md | 14 ++++++++------ published/function-autoloading.ptxt | 4 +++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/drafts/function-autoloading.md b/drafts/function-autoloading.md index 283e4b9..3c6635b 100644 --- a/drafts/function-autoloading.md +++ b/drafts/function-autoloading.md @@ -82,9 +82,12 @@ it may be desirable to stick with FQNs for functions and/or employ caches and ot ### spl_autoload -The `spl_autoload` function will not be updated. -For backwards compatibility, -there will be no changes to class autoloading and there will not be a default function autoloader. +`spl_autoload`'s second argument will be updated to accept `int|string|null` as the second parameter so that it can use +the new callback signature. +If the second parameter is an int, and it is not `SPL_AUTOLOAD_CLASS`, +an `Error` is thrown: 'Default autoloader can only load classes.' + +There will not be a default function autoloader. ### spl_autoload_call @@ -103,12 +106,12 @@ spl_autoload_call('func', SPL_AUTOLOAD_FUNCTION | SPL_AUTOLOAD_CLASS); // Error: If the user wants to call multiple autoloaders, they can do so manually. - ### function_exists The `function_exists` function will be updated to include a boolean option (`$autoload`) as the second parameter, which will default to `true`. -If set to `true`, the function autoloader will be called if the function is not defined, otherwise, it will not be called. +If set to `true`, the function autoloader will be called if the function is not defined, otherwise, it will not be +called. ## Backward Incompatible Changes @@ -159,7 +162,6 @@ specification section (if any) ## References - - [autofunc](https://wiki.php.net/rfc/autofunc): This heavily influenced this RFC. (declined in 2011) - [function_autoloading](https://wiki.php.net/rfc/function_autoloading): This RFC was declined in 2011. - [function_autoloading_v2](https://wiki.php.net/rfc/function_autoloading2): This RFC was declined in 2012. diff --git a/published/function-autoloading.ptxt b/published/function-autoloading.ptxt index 0741021..f605938 100644 --- a/published/function-autoloading.ptxt +++ b/published/function-autoloading.ptxt @@ -70,7 +70,9 @@ For codebases that want to take advantage of function autoloading, it may be des ==== spl_autoload ==== -The ''%%spl_autoload%%'' function will not be updated. For backwards compatibility, there will be no changes to class autoloading and there will not be a default function autoloader. +''%%spl_autoload%%'''s second argument will be updated to accept ''%%int|string|null%%'' as the second parameter so that it can use the new callback signature. If the second parameter is an int, and it is not ''%%SPL_AUTOLOAD_CLASS%%'', an ''%%Error%%'' is thrown: 'Default autoloader can only load classes.' + +There will not be a default function autoloader. ==== spl_autoload_call ==== From c6cbaf0760b5f26a538b125763c886aa90418549 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 16 Nov 2024 22:56:13 +0100 Subject: [PATCH 39/46] updated rfc --- drafts/records.md | 196 +++++++++++++++++++++++++++------------------- 1 file changed, 117 insertions(+), 79 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 590386a..c2e352d 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -14,7 +14,7 @@ with [value semantics](https://en.wikipedia.org/wiki/Value_semantics). ### Value objects 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. +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. @@ -100,6 +100,11 @@ 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. +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. + ``` php namespace Paint; @@ -149,9 +154,29 @@ record PaintBucket(StockPaint ...$constituents) { #### Usage -A record may be used as a readonly class, +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. +assisting in migrating from one implementation to another: + +```php +$gray = $bucket->mixIn($blackPaint)->mixIn($whitePaint); +``` + +Records are instantiated in a function format, with `&` prepended. +This provides visual feedback that a record is being created instead of a function call. + +```php +$black = &Pigment(0, 0, 0); +$white = &Pigment(255, 255, 255); +$blackPaint = &StockPaint($black, 1); +$whitePaint = &StockPaint($white, 1); +$bucket = &PaintBucket(); + +$gray = $bucket->mixIn($blackPaint)->mixIn($whitePaint); +$grey = $bucket->mixIn($blackPaint)->mixIn($whitePaint); + +assert($gray === $grey); // true +``` #### Optional parameters and default values @@ -164,7 +189,7 @@ it is optional when instantiating the record, and PHP will assign the default va ``` php record Rectangle(int $x, int $y = 10); -var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 +var_dump(&Rectangle(10)); // output a record with x: 10 and y: 10 ``` #### Auto-generated `with` method @@ -196,7 +221,7 @@ record UserId(int $id) { } } -$userId = UserId(1); +$userId = &UserId(1); $otherId = $userId->with(2); // Fails: Named arguments must be used $otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not defined in the inline constructor $otherId = $userId->with(id: 2); // Success: id is updated @@ -207,7 +232,7 @@ Using variadic arguments: ```php record Vector(int $dimensions, int ...$values); -$vector = Vector(3, 1, 2, 3); +$vector = &Vector(3, 1, 2, 3); $vector = $vector->with(dimensions: 4); // Success: values are updated $vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: mixing named arguments with variadic arguments is not allowed by PHP syntax $vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // Success: First update dimensions, then values @@ -217,11 +242,7 @@ $vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // Success: First upda 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. - -Contravariance and covariance are enforced in the developer’s code via the `Record` interface: -- Contravariance: the parameter type of the custom `with` method must be a supertype of the generated `with` method. -- Covariance: the return type of the custom `with` method must be `self` of the generated `with` method. +This allows a developer to define policies or constraints on how data can change from instance to instance. ``` php record Planet(string $name, int $population) { @@ -251,25 +272,26 @@ and are mutable until the end of the method, at which point they become immutable. ```php -// Inline constructor -record User(string $name, string $email) { +// Inline constructor defining two properties +record User(string $name, string $emailAddress) { public string $id; // Traditional constructor public function __construct() { - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + if (!is_valid_email($this->emailAddress)) { throw new InvalidArgumentException("Invalid email address"); } - $this->id = hash('sha256', $email); - $this->name = ucwords($name); + $this->id = hash('sha256', $this->emailAddress); + $this->name = ucwords($this->name); + // all properties are now immutable } } ``` ### Mental models and how it works -From the perspective of a developer, declaring a record declares an object and function with the same name. +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. @@ -277,19 +299,32 @@ For example, this would be a valid mental model for a Point record: ```php record Point(int $x, int $y) { + public float $magnitude; + + public function __construct() { + $this->magnitude = sqrt($this->x ** 2 + $this->y ** 2); + } + public function add(Point $point): Point { - return Point($this->x + $point->x, $this->y + $point->y); + return &Point($this->x + $point->x, $this->y + $point->y); + } + + public function dot(Point $point): int { + return $this->x * $point->x + $this->y * $point->y; } } // similar to declaring the following function and class -// used during construction to allow immutability +// used during construction to allow mutability class Point_Implementation { public int $x; public int $y; + public float $magnitude; - public function __construct() {} + public function __construct() { + $this->magnitude = sqrt($this->x ** 2 + $this->y ** 2); + } public function with(...$parameters) { // validity checks omitted for brevity @@ -300,14 +335,16 @@ class Point_Implementation { public function add(Point $point): Point { return Point($this->x + $point->x, $this->y + $point->y); } + + public function dot(Point $point): int { + return $this->x * $point->x + $this->y * $point->y; + } } -interface Record { - public function with(...$parameters): self; -} +// used to enforce immutability but has nearly the same implementation +readonly class Point { + public float $magnitude; -// used to enforce immutability but has the same implementation -readonly class Point implements Record { public function __construct(public int $x, public int $y) {} public function with(...$parameters): self { @@ -319,46 +356,52 @@ readonly class Point implements Record { public function add(Point $point): Point { return Point($this->x + $point->x, $this->y + $point->y); } + + public function dot(Point $point): int { + return $this->x * $point->x + $this->y * $point->y; + } } function Point(int $x, int $y): Point { static $points = []; - // look up the identity of the point - $key = hash_func($x, $y); + + $key = hash_object($mutablePoint); if ($points[$key] ?? null) { // return an existing point return $points[$key]; } - + // create a new point $reflector = new \ReflectionClass(Point_Implementation::class); - $point = $reflector->newInstanceWithoutConstructor(); - $point->x = $x; - $point->y = $y; - $point->__construct(); - // copy properties to an immutable point and return it - $point = new Point($point->x, $point->y); + $mutablePoint = $reflector->newInstanceWithoutConstructor(); + $mutablePoint->x = $x; + $mutablePoint->y = $y; + $mutablePoint->__construct(); + + // copy properties to an immutable Point and return it + $point = new Point($mutablePoint->x, $mutablePoint->y); + $point->magnitude = $mutablePoint->magnitude; return $points[$key] = $point; } ``` 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. ### Performance considerations 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. +longer referenced. ``` php -$point1 = Point(3, 4); +$point1 = &Point(3, 4); $point2 = $point1; // No data duplication, $point2 references the same data as $point1 $point3 = Point(3, 4); // No data duplication, it is pointing to the same memory as $point1 $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance +$point5 = &Point(5, 4); // No data duplication, it is pointing to the same memory as $point4 ``` #### Cloning and with() @@ -366,26 +409,25 @@ $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new i 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`. -`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)`. +If `->with()` is called with no arguments, a warning will be emitted, as this is most likely a mistake. ### Serialization and deserialization -Records are fully serializable and deserializable. +Records are fully serializable and deserializable, even when nested. ```php record Single(string $value); record Multiple(string $value1, string $value2); -echo $single = serialize(Single('value')); // Outputs: "O:6:"Single":1:{s:5:"value";s:5:"value";}" -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";}}" +echo $single = serialize(&Single('value')); // Outputs: "O:6:"Single":1:{s:5:"value";s:5:"value";}" +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";}}" -echo unserialize($single) === Single('value'); // Outputs: true -echo unserialize($multiple) === Multiple('value1', 'value2'); // Outputs: true +echo unserialize($single) === &Single('value'); // Outputs: true +echo unserialize($multiple) === &Multiple('value1', 'value2'); // Outputs: true ``` +If a record contains objects or values that are unserializable, the record will not be serializable. + ### Equality A `record` is always strongly equal (`===`) to another record with the same value in the properties, @@ -397,7 +439,7 @@ Comparison operations will behave exactly like they do for classes, which is cur #### Non-trivial values For non-trivial values (e.g., objects, closures, resources, etc.), -the `===` operator will return `true` if the two operands reference the same object. +the `===` operator will return `true` if the two operands reference the same instances. 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: @@ -414,7 +456,9 @@ $dateRecord2 = Date($date2); echo $dateRecord1 === $dateRecord2; // Outputs: false ``` -However, this can be worked around by being a bit creative (see: mental model): +However, +this can be worked around by being a bit creative (see: mental model) +as only the values passed in the constructor are compared: ```php record Date(string $date) { @@ -425,10 +469,10 @@ record Date(string $date) { } } -$date1 = Date('2024-07-19'); -$date2 = Date('2024-07-19'); +$date1 = &Date('2024-07-19'); +$date2 = &Date('2024-07-19'); -echo $date1->datetime === $date2->datetime; // Outputs: true +echo $date1->datetime === $date2->datetime ? 'true' : 'false'; // Outputs: true ``` ### Type hinting @@ -470,26 +514,32 @@ Calling `finalizeRecord()` on a record that has already been finalized will retu #### isRecord() -The `isRecord()` method is used to determine if an object is a record. It returns `true` if the object is a record, +The `isRecord()` method is used to determine if an object is a record. It returns `true` if the object is a record. #### getInlineConstructor() 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. +Invoking the `invoke()` method on the `ReflectionFunction` will create a finalized record. + #### getTraditionalConstructor() 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. +Invoking the `invoke()` method on the `ReflectionMethod` on a finalized record will throw an exception. + #### makeMutable() 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. -A mutable record can be finalized again using `finalizeRecord()` and to the engine, these are regular classes. +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`. #### isMutable() @@ -497,6 +547,7 @@ For example, `var_dump()` will output `object` instead of `record`. The `isMutable()` method is used to determine if a record has been made mutable via `makeMutable()` or otherwise not yet finalized. + #### Custom deserialization example In cases where custom deserialization is required, @@ -505,10 +556,10 @@ a developer can use `ReflectionRecord` to manually construct a new instance of a ```php record Seconds(int $seconds); -$example = Seconds(5); +$example = &Seconds(5); -$reflector = new ReflectionRecord(ExpirationDate::class); -$expiration = $reflector->newInstanceWithoutConstructor(); +$reflector = new ReflectionRecord(Seconds::class); +$expiration = $reflector->newInstanceWithoutConstructor(); // this is a mutable object $expiration->seconds = 5; assert($example !== $expiration); // true $expiration = $reflector->finalizeRecord($expiration); @@ -533,28 +584,21 @@ record(Point)#1 (2) { ### Considerations for implementations -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. +A `record` cannot share its name with an existing `record`, +`class`, `interface`, `trait`, or `function`, just like a class. ### Autoloading -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. - -Once function autoloading is implemented in PHP at some hopeful point in the future, -said autoloader could locate the record and then autoload it. - -The author of this RFC strongly encourages someone to put forward a function autoloading RFC if autoloading is desired -for records. +Records will be autoloaded in the same way as classes. ## Backward Incompatible Changes To avoid conflicts with existing code, the `record` keyword will be handled similarly to `enum` to prevent backward compatibility issues. +Since `&` is currently a syntax error when prefixed on a function call, +it will be used to denote a record instantiation. + ## Proposed PHP Version(s) PHP 8.5 @@ -595,8 +639,7 @@ None. ## Proposed Voting Choices -Include these so readers know where you’re heading and can discuss the -proposed voting options. +2/3 majority. ## Patches and Tests @@ -604,17 +647,12 @@ TBD ## Implementation -After the project is implemented, this section should contain - -1. the version(s) it was merged into -2. a link to the git commit(s) -3. a link to the PHP manual entry for the feature -4. a link to the language specification section (if any) +To be completed during a later phase of discussion. ## References -Links to external references, discussions or RFCs +- [Value semantics](https://en.wikipedia.org/wiki/Value_semantics) ## Rejected Features -Keep this updated with features that were discussed on the mail lists. +TBD From 6cd68e681e02b7062c25ecd6255800c95a3a9691 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 16 Nov 2024 22:56:59 +0100 Subject: [PATCH 40/46] updated rfc --- published/records.ptxt | 174 +++++++++++++++++++++++++---------------- 1 file changed, 106 insertions(+), 68 deletions(-) diff --git a/published/records.ptxt b/published/records.ptxt index 8fd50d7..1bde378 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -12,7 +12,7 @@ This RFC proposes the introduction of ''%%record%%'' objects, which are immutabl ==== Value objects ==== -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. +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. Consider this example where a function accepts an integer as a user ID, and the ID is accidentally set to a nonsensical value: @@ -76,6 +76,8 @@ A **record** body may also declare properties whose values are only mutable duri 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. +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. + namespace Paint; @@ -125,7 +127,26 @@ record PaintBucket(StockPaint ...$constituents) { === Usage === -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. +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: + + +$gray = $bucket->mixIn($blackPaint)->mixIn($whitePaint); + + +Records are instantiated in a function format, with ''%%&%%'' prepended. This provides visual feedback that a record is being created instead of a function call. + + +$black = &Pigment(0, 0, 0); +$white = &Pigment(255, 255, 255); +$blackPaint = &StockPaint($black, 1); +$whitePaint = &StockPaint($white, 1); +$bucket = &PaintBucket(); + +$gray = $bucket->mixIn($blackPaint)->mixIn($whitePaint); +$grey = $bucket->mixIn($blackPaint)->mixIn($whitePaint); + +assert($gray === $grey); // true + === Optional parameters and default values === @@ -135,7 +156,7 @@ One or more properties defined in the inline constructor may have a default valu record Rectangle(int $x, int $y = 10); -var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 +var_dump(&Rectangle(10)); // output a record with x: 10 and y: 10 === Auto-generated with method === @@ -163,7 +184,7 @@ record UserId(int $id) { } } -$userId = UserId(1); +$userId = &UserId(1); $otherId = $userId->with(2); // Fails: Named arguments must be used $otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not defined in the inline constructor $otherId = $userId->with(id: 2); // Success: id is updated @@ -174,7 +195,7 @@ Using variadic arguments: record Vector(int $dimensions, int ...$values); -$vector = Vector(3, 1, 2, 3); +$vector = &Vector(3, 1, 2, 3); $vector = $vector->with(dimensions: 4); // Success: values are updated $vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: mixing named arguments with variadic arguments is not allowed by PHP syntax $vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // Success: First update dimensions, then values @@ -182,12 +203,7 @@ $vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // Success: First upda == Custom with method == -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. - -Contravariance and covariance are enforced in the developer’s code via the ''%%Record%%'' interface: - - * Contravariance: the parameter type of the custom ''%%with%%'' method must be a supertype of the generated ''%%with%%'' method. - * Covariance: the return type of the custom ''%%with%%'' method must be ''%%self%%'' of the generated ''%%with%%'' method. +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. record Planet(string $name, int $population) { @@ -212,43 +228,57 @@ The inline constructor is always required and must define at least one parameter 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. -// Inline constructor -record User(string $name, string $email) { +// Inline constructor defining two properties +record User(string $name, string $emailAddress) { public string $id; // Traditional constructor public function __construct() { - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + if (!is_valid_email($this->emailAddress)) { throw new InvalidArgumentException("Invalid email address"); } - $this->id = hash('sha256', $email); - $this->name = ucwords($name); + $this->id = hash('sha256', $this->emailAddress); + $this->name = ucwords($this->name); + // all properties are now immutable } } ==== Mental models and how it works ==== -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. +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. For example, this would be a valid mental model for a Point record: record Point(int $x, int $y) { + public float $magnitude; + + public function __construct() { + $this->magnitude = sqrt($this->x ** 2 + $this->y ** 2); + } + public function add(Point $point): Point { - return Point($this->x + $point->x, $this->y + $point->y); + return &Point($this->x + $point->x, $this->y + $point->y); + } + + public function dot(Point $point): int { + return $this->x * $point->x + $this->y * $point->y; } } // similar to declaring the following function and class -// used during construction to allow immutability +// used during construction to allow mutability class Point_Implementation { public int $x; public int $y; + public float $magnitude; - public function __construct() {} + public function __construct() { + $this->magnitude = sqrt($this->x ** 2 + $this->y ** 2); + } public function with(...$parameters) { // validity checks omitted for brevity @@ -259,14 +289,16 @@ class Point_Implementation { public function add(Point $point): Point { return Point($this->x + $point->x, $this->y + $point->y); } + + public function dot(Point $point): int { + return $this->x * $point->x + $this->y * $point->y; + } } -interface Record { - public function with(...$parameters): self; -} +// used to enforce immutability but has nearly the same implementation +readonly class Point { + public float $magnitude; -// used to enforce immutability but has the same implementation -readonly class Point implements Record { public function __construct(public int $x, public int $y) {} public function with(...$parameters): self { @@ -278,64 +310,73 @@ readonly class Point implements Record { public function add(Point $point): Point { return Point($this->x + $point->x, $this->y + $point->y); } + + public function dot(Point $point): int { + return $this->x * $point->x + $this->y * $point->y; + } } function Point(int $x, int $y): Point { static $points = []; - // look up the identity of the point - $key = hash_func($x, $y); + + $key = hash_object($mutablePoint); if ($points[$key] ?? null) { // return an existing point return $points[$key]; } - + // create a new point $reflector = new \ReflectionClass(Point_Implementation::class); - $point = $reflector->newInstanceWithoutConstructor(); - $point->x = $x; - $point->y = $y; - $point->__construct(); - // copy properties to an immutable point and return it - $point = new Point($point->x, $point->y); + $mutablePoint = $reflector->newInstanceWithoutConstructor(); + $mutablePoint->x = $x; + $mutablePoint->y = $y; + $mutablePoint->__construct(); + + // copy properties to an immutable Point and return it + $point = new Point($mutablePoint->x, $mutablePoint->y); + $point->magnitude = $mutablePoint->magnitude; return $points[$key] = $point; } -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. +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. ==== Performance considerations ==== -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. +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. -$point1 = Point(3, 4); +$point1 = &Point(3, 4); $point2 = $point1; // No data duplication, $point2 references the same data as $point1 $point3 = Point(3, 4); // No data duplication, it is pointing to the same memory as $point1 $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance +$point5 = &Point(5, 4); // No data duplication, it is pointing to the same memory as $point4 === Cloning and with() === 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%%''. -''%%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)%%''. +If ''%%->with()%%'' is called with no arguments, a warning will be emitted, as this is most likely a mistake. ==== Serialization and deserialization ==== -Records are fully serializable and deserializable. +Records are fully serializable and deserializable, even when nested. record Single(string $value); record Multiple(string $value1, string $value2); -echo $single = serialize(Single('value')); // Outputs: "O:6:"Single":1:{s:5:"value";s:5:"value";}" -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";}}" +echo $single = serialize(&Single('value')); // Outputs: "O:6:"Single":1:{s:5:"value";s:5:"value";}" +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";}}" -echo unserialize($single) === Single('value'); // Outputs: true -echo unserialize($multiple) === Multiple('value1', 'value2'); // Outputs: true +echo unserialize($single) === &Single('value'); // Outputs: true +echo unserialize($multiple) === &Multiple('value1', 'value2'); // Outputs: true +If a record contains objects or values that are unserializable, the record will not be serializable. + ==== Equality ==== 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 === Non-trivial values === -For non-trivial values (e.g., objects, closures, resources, etc.), the ''%%===%%'' operator will return ''%%true%%'' if the two operands reference the same object. +For non-trivial values (e.g., objects, closures, resources, etc.), the ''%%===%%'' operator will return ''%%true%%'' if the two operands reference the same instances. 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: @@ -360,7 +401,7 @@ $dateRecord2 = Date($date2); echo $dateRecord1 === $dateRecord2; // Outputs: false -However, this can be worked around by being a bit creative (see: mental model): +However, this can be worked around by being a bit creative (see: mental model) as only the values passed in the constructor are compared: record Date(string $date) { @@ -371,10 +412,10 @@ record Date(string $date) { } } -$date1 = Date('2024-07-19'); -$date2 = Date('2024-07-19'); +$date1 = &Date('2024-07-19'); +$date2 = &Date('2024-07-19'); -echo $date1->datetime === $date2->datetime; // Outputs: true +echo $date1->datetime === $date2->datetime ? 'true' : 'false'; // Outputs: true ==== Type hinting ==== @@ -412,21 +453,25 @@ Calling ''%%finalizeRecord()%%'' on a record that has already been finalized wil === isRecord() === -The ''%%isRecord()%%'' method is used to determine if an object is a record. It returns ''%%true%%'' if the object is a record, +The ''%%isRecord()%%'' method is used to determine if an object is a record. It returns ''%%true%%'' if the object is a record. === getInlineConstructor() === 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. +Invoking the ''%%invoke()%%'' method on the ''%%ReflectionFunction%%'' will create a finalized record. + === getTraditionalConstructor() === 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. +Invoking the ''%%invoke()%%'' method on the ''%%ReflectionMethod%%'' on a finalized record will throw an exception. + === makeMutable() === 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. -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%%''. +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%%''. === isMutable() === @@ -439,10 +484,10 @@ In cases where custom deserialization is required, a developer can use ''%%Refle record Seconds(int $seconds); -$example = Seconds(5); +$example = &Seconds(5); -$reflector = new ReflectionRecord(ExpirationDate::class); -$expiration = $reflector->newInstanceWithoutConstructor(); +$reflector = new ReflectionRecord(Seconds::class); +$expiration = $reflector->newInstanceWithoutConstructor(); // this is a mutable object $expiration->seconds = 5; assert($example !== $expiration); // true $expiration = $reflector->finalizeRecord($expiration); @@ -464,20 +509,18 @@ record(Point)#1 (2) { ==== Considerations for implementations ==== -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. +A ''%%record%%'' cannot share its name with an existing ''%%record%%'', ''%%class%%'', ''%%interface%%'', ''%%trait%%'', or ''%%function%%'', just like a class. ==== Autoloading ==== -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. - -Once function autoloading is implemented in PHP at some hopeful point in the future, said autoloader could locate the record and then autoload it. - -The author of this RFC strongly encourages someone to put forward a function autoloading RFC if autoloading is desired for records. +Records will be autoloaded in the same way as classes. ===== Backward Incompatible Changes ===== To avoid conflicts with existing code, the ''%%record%%'' keyword will be handled similarly to ''%%enum%%'' to prevent backward compatibility issues. +Since ''%%&%%'' is currently a syntax error when prefixed on a function call, it will be used to denote a record instantiation. + ===== Proposed PHP Version(s) ===== PHP 8.5 @@ -518,7 +561,7 @@ None. ===== Proposed Voting Choices ===== -Include these so readers know where you’re heading and can discuss the proposed voting options. +2/3 majority. ===== Patches and Tests ===== @@ -526,17 +569,12 @@ TBD ===== Implementation ===== -After the project is implemented, this section should contain - - - the version(s) it was merged into - - a link to the git commit(s) - - a link to the PHP manual entry for the feature - - a link to the language specification section (if any) +To be completed during a later phase of discussion. ===== References ===== -Links to external references, discussions or RFCs + * [[https://en.wikipedia.org/wiki/Value_semantics|Value semantics]] ===== Rejected Features ===== -Keep this updated with features that were discussed on the mail lists. +TBD From fdb8dd63ec79cc2038c5bca8307471a4cb6b953a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 16 Nov 2024 21:57:24 +0000 Subject: [PATCH 41/46] Automated changes from GitHub Actions --- published/function-autoloading.ptxt | 6 ------ published/template.ptxt | 6 +++--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/published/function-autoloading.ptxt b/published/function-autoloading.ptxt index f605938..2f2d4ad 100644 --- a/published/function-autoloading.ptxt +++ b/published/function-autoloading.ptxt @@ -123,12 +123,6 @@ Potentially, constants and stream wrappers can be added in a similar fashion. ===== Proposed Voting Choices ===== - - * Yes - * No - - - ===== Patches and Tests ===== Not yet. diff --git a/published/template.ptxt b/published/template.ptxt index 3f379b3..960ba82 100644 --- a/published/template.ptxt +++ b/published/template.ptxt @@ -7,11 +7,11 @@ This is a suggested template for PHP Request for Comments (RFCs). Change this te Quoting [[http://news.php.net/php.internals/71525|Rasmus]]: > PHP is and should remain: -> + > 1) a pragmatic web-focused language -> + > 2) a loosely typed language -> + > 3) a language which caters to the skill-levels and platforms of a wide range of users Your RFC should move PHP forward following his vision. As [[http://news.php.net/php.internals/66065|said by Zeev Suraski]] "Consider only features which have significant traction to a large chunk of our userbase, and not something that could be useful in some extremely specialized edge cases [...] Make sure you think about the full context, the huge audience out there, the consequences of making the learning curve steeper with every new feature, and the scope of the goodness that those new features bring." From f14b0930eba108c0a49e05fc86b2729743098589 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 16 Nov 2024 22:58:39 +0100 Subject: [PATCH 42/46] updated author --- drafts/records.md | 2 +- published/records.ptxt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index c2e352d..7cfd58f 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -2,7 +2,7 @@ - Version: 0.9 - Date: 2024-07-19 -- Author: Robert Landers, landers.robert@gmail.com +- Author: Robert Landers, landers.robert@gmail.com, rob@bottled.codes - Status: Draft (or Under Discussion or Accepted or Declined) - First Published at: diff --git a/published/records.ptxt b/published/records.ptxt index 1bde378..032d57a 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -2,7 +2,7 @@ * Version: 0.9 * Date: 2024-07-19 - * Author: Robert Landers, + * Author: Robert Landers, , * Status: Draft (or Under Discussion or Accepted or Declined) * First Published at: http://wiki.php.net/rfc/records From c7155c39b40bf380a4a63745e5a212ec81dcebcf Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 16 Nov 2024 23:08:09 +0100 Subject: [PATCH 43/46] added interface documentation --- drafts/records.md | 40 ++++++++++++++++++++++++++++++++++++++++ published/records.ptxt | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/drafts/records.md b/drafts/records.md index 7cfd58f..28d32fe 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -289,6 +289,46 @@ record User(string $name, string $emailAddress) { } ``` +### Implementing Interfaces + +A **record** can implement interfaces, but it cannot extend other records or classes, but may use traits: + +```php +interface Vehicle {} + +interface Car extends Vehicle { + public function drive(): void; +} + +interface SpaceShip extends Vehicle { + public function launch(): void; +} + +record FancyCar(string $model) implements Car { + public function drive(): void { + echo "Driving a Fancy Car {$this->model}"; + } +} + +record SpaceCar(string $model) implements Car, SpaceShip { + public function drive(): void { + echo "Driving a Space Car {$this->model}"; + } + + public function launch(): void { + echo "Launching a Space Car {$this->model}"; + } +} + +record Submarine(string $model) implements Vehicle { + use Submersible; +} + +record TowTruct(string $model, private Car $towing) implements Car { + use Towable; +} +``` + ### Mental models and how it works From the perspective of a developer, declaring a record declares an object with the same name. diff --git a/published/records.ptxt b/published/records.ptxt index 032d57a..c839785 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -245,6 +245,46 @@ record User(string $name, string $emailAddress) { } +==== Implementing Interfaces ==== + +A **record** can implement interfaces, but it cannot extend other records or classes, but may use traits: + + +interface Vehicle {} + +interface Car extends Vehicle { + public function drive(): void; +} + +interface SpaceShip extends Vehicle { + public function launch(): void; +} + +record FancyCar(string $model) implements Car { + public function drive(): void { + echo "Driving a Fancy Car {$this->model}"; + } +} + +record SpaceCar(string $model) implements Car, SpaceShip { + public function drive(): void { + echo "Driving a Space Car {$this->model}"; + } + + public function launch(): void { + echo "Launching a Space Car {$this->model}"; + } +} + +record Submarine(string $model) implements Vehicle { + use Submersible; +} + +record TowTruct(string $model, private Car $towing) implements Car { + use Towable; +} + + ==== Mental models and how it works ==== 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. From 02ed26fb67c78304ad8991718c8bf5ae28df132e Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 16 Nov 2024 23:38:58 +0100 Subject: [PATCH 44/46] update records --- drafts/records.md | 5 +++-- published/records.ptxt | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 28d32fe..2be7ec3 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -551,10 +551,12 @@ The `finalizeRecord()` method is used to make a record immutable and look up its returning an instance that represents the finalized record. Calling `finalizeRecord()` on a record that has already been finalized will return the same instance. +Attempting to finalize a regular object will throw a `ReflectionException`. #### isRecord() -The `isRecord()` method is used to determine if an object is a record. It returns `true` if the object is a record. +The `isRecord()` method is used to determine if an object is a record. +It returns `true` if the object is a finalized record. #### getInlineConstructor() @@ -587,7 +589,6 @@ For example, `var_dump()` will output `object` instead of `record`. The `isMutable()` method is used to determine if a record has been made mutable via `makeMutable()` or otherwise not yet finalized. - #### Custom deserialization example In cases where custom deserialization is required, diff --git a/published/records.ptxt b/published/records.ptxt index c839785..6073fc2 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -489,11 +489,11 @@ Attempting to use ''%%ReflectionClass%%'' or ''%%ReflectionFunction%%'' on a rec The ''%%finalizeRecord()%%'' method is used to make a record immutable and look up its value in the internal cache, returning an instance that represents the finalized record. -Calling ''%%finalizeRecord()%%'' on a record that has already been finalized will return the same instance. +Calling ''%%finalizeRecord()%%'' on a record that has already been finalized will return the same instance. Attempting to finalize a regular object will throw a ''%%ReflectionException%%''. === isRecord() === -The ''%%isRecord()%%'' method is used to determine if an object is a record. It returns ''%%true%%'' if the object is a record. +The ''%%isRecord()%%'' method is used to determine if an object is a record. It returns ''%%true%%'' if the object is a finalized record. === getInlineConstructor() === From be0024a09ed3e084f902868b062dabba25210574 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 16 Nov 2024 23:40:37 +0100 Subject: [PATCH 45/46] move to under discussion --- drafts/records.md | 2 +- published/records.ptxt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/drafts/records.md b/drafts/records.md index 2be7ec3..c2dfbf9 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -3,7 +3,7 @@ - Version: 0.9 - Date: 2024-07-19 - Author: Robert Landers, landers.robert@gmail.com, rob@bottled.codes -- Status: Draft (or Under Discussion or Accepted or Declined) +- Status: Under Discussion (or Accepted or Declined) - First Published at: ## Introduction diff --git a/published/records.ptxt b/published/records.ptxt index 6073fc2..ce6af34 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -3,7 +3,7 @@ * Version: 0.9 * Date: 2024-07-19 * Author: Robert Landers, , - * Status: Draft (or Under Discussion or Accepted or Declined) + * Status: Under Discussion (or Accepted or Declined) * First Published at: http://wiki.php.net/rfc/records ===== Introduction ===== From c2e5eefd6bb509dd6b5cf103ba12672fabb117e1 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 17 Nov 2024 00:10:56 +0100 Subject: [PATCH 46/46] new functions --- drafts/records.md | 4 ++++ published/records.ptxt | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/drafts/records.md b/drafts/records.md index c2dfbf9..16b6a09 100644 --- a/drafts/records.md +++ b/drafts/records.md @@ -632,6 +632,10 @@ A `record` cannot share its name with an existing `record`, Records will be autoloaded in the same way as classes. +### New Functions + +- `record_exists` will return `true` if a record exists and `false` otherwise. It has the same signature as `class_exists`. + ## Backward Incompatible Changes To avoid conflicts with existing code, diff --git a/published/records.ptxt b/published/records.ptxt index ce6af34..695b7ba 100644 --- a/published/records.ptxt +++ b/published/records.ptxt @@ -555,6 +555,10 @@ A ''%%record%%'' cannot share its name with an existing ''%%record%%'', ''%%clas Records will be autoloaded in the same way as classes. +==== New Functions ==== + + * ''%%record_exists%%'' will return ''%%true%%'' if a record exists and ''%%false%%'' otherwise. It has the same signature as ''%%class_exists%%''. + ===== Backward Incompatible Changes ===== To avoid conflicts with existing code, the ''%%record%%'' keyword will be handled similarly to ''%%enum%%'' to prevent backward compatibility issues.