rfc:records
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
rfc:records [2024/08/02 18:51] – add complex types withinboredom | rfc:records [2024/11/17 21:38] (current) – withinboredom | ||
---|---|---|---|
Line 3: | Line 3: | ||
* Version: 0.9 | * Version: 0.9 | ||
* Date: 2024-07-19 | * Date: 2024-07-19 | ||
- | * Author: Robert Landers, < | + | * Author: Robert Landers, < |
- | * Status: | + | * Status: Under Discussion |
* First Published at: http:// | * First Published at: http:// | ||
Line 13: | Line 13: | ||
==== Value objects ==== | ==== Value objects ==== | ||
- | Value objects are immutable objects that represent a value. They’re used to store values with a different semantic | + | Value objects are immutable objects that represent a value. They’re used to store values with a different semantic |
Consider this example where a function accepts an integer as a user ID, and the ID is accidentally set to a nonsensical value: | Consider this example where a function accepts an integer as a user ID, and the ID is accidentally set to a nonsensical value: | ||
Line 76: | Line 76: | ||
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 '' | 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 '' | ||
+ | |||
+ | As an example, the following code defines a **record** named '' | ||
<code php> | <code php> | ||
Line 126: | Line 128: | ||
=== Usage === | === Usage === | ||
- | A record may be used as a readonly | + | 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: |
+ | |||
+ | <code php> | ||
+ | $gray = $bucket-> | ||
+ | </ | ||
+ | |||
+ | Records are instantiated in a function format, with '' | ||
+ | |||
+ | <code php> | ||
+ | $black = & | ||
+ | $white = & | ||
+ | $blackPaint = & | ||
+ | $whitePaint = & | ||
+ | $bucket = & | ||
+ | |||
+ | $gray = $bucket-> | ||
+ | $grey = $bucket-> | ||
+ | |||
+ | assert($gray === $grey); // true | ||
+ | </ | ||
=== Optional parameters and default values === | === Optional parameters and default values === | ||
Line 136: | Line 157: | ||
<code php> | <code php> | ||
record Rectangle(int $x, int $y = 10); | record Rectangle(int $x, int $y = 10); | ||
- | var_dump(Rectangle(10)); | + | var_dump(&Rectangle(10)); |
</ | </ | ||
Line 164: | Line 185: | ||
} | } | ||
- | $userId = UserId(1); | + | $userId = &UserId(1); |
$otherId = $userId-> | $otherId = $userId-> | ||
$otherId = $userId-> | $otherId = $userId-> | ||
Line 175: | Line 196: | ||
record Vector(int $dimensions, | record Vector(int $dimensions, | ||
- | $vector = Vector(3, 1, 2, 3); | + | $vector = &Vector(3, 1, 2, 3); |
$vector = $vector-> | $vector = $vector-> | ||
$vector = $vector-> | $vector = $vector-> | ||
Line 183: | Line 204: | ||
== Custom with method == | == Custom with method == | ||
- | A developer may define their own '' | + | A developer may define their own '' |
- | + | ||
- | Contravariance and covariance are enforced in the developer’s code via the '' | + | |
- | + | ||
- | * Contravariance: | + | |
- | * Covariance: the return type of the custom '' | + | |
<code php> | <code php> | ||
Line 213: | Line 229: | ||
<code php> | <code php> | ||
- | // Inline constructor | + | // Inline constructor |
- | record User(string $name, string $email) { | + | record User(string $name, string $emailAddress) { |
public string $id; | public string $id; | ||
// Traditional constructor | // Traditional constructor | ||
public function __construct() { | public function __construct() { | ||
- | if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { | + | if (!is_valid_email($this-> |
throw new InvalidArgumentException(" | throw new InvalidArgumentException(" | ||
} | } | ||
| | ||
- | $this-> | + | $this-> |
- | $this-> | + | $this-> |
+ | // all properties are now immutable | ||
} | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Implementing Interfaces ==== | ||
+ | |||
+ | A **record** can implement interfaces, but it cannot extend other records or classes, but may use traits: | ||
+ | |||
+ | <code 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 " | ||
+ | } | ||
+ | } | ||
+ | |||
+ | record SpaceCar(string $model) implements Car, SpaceShip { | ||
+ | public function drive(): void { | ||
+ | echo " | ||
+ | } | ||
+ | | ||
+ | public function launch(): void { | ||
+ | echo " | ||
+ | } | ||
+ | } | ||
+ | |||
+ | record Submarine(string $model) implements Vehicle { | ||
+ | use Submersible; | ||
+ | } | ||
+ | |||
+ | record TowTruct(string $model, private Car $towing) implements Car { | ||
+ | use Towable; | ||
} | } | ||
</ | </ | ||
Line 231: | Line 288: | ||
==== Mental models and how it works ==== | ==== Mental models and how it works ==== | ||
- | From the perspective of a developer, declaring a record declares an object | + | 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: | For example, this would be a valid mental model for a Point record: | ||
Line 237: | Line 294: | ||
<code php> | <code php> | ||
record Point(int $x, int $y) { | record Point(int $x, int $y) { | ||
+ | public float $magnitude; | ||
+ | | ||
+ | public function __construct() { | ||
+ | $this-> | ||
+ | } | ||
+ | |||
public function add(Point $point): Point { | public function add(Point $point): Point { | ||
- | return Point($this-> | + | return |
+ | } | ||
+ | |||
+ | public function dot(Point $point): int { | ||
+ | return $this->x * $point-> | ||
} | } | ||
} | } | ||
Line 244: | Line 311: | ||
// similar to declaring the following function and class | // similar to declaring the following function and class | ||
- | // used during construction to allow immutability | + | // used during construction to allow mutability |
class Point_Implementation { | class Point_Implementation { | ||
public int $x; | public int $x; | ||
public int $y; | public int $y; | ||
+ | public float $magnitude; | ||
- | public function __construct() {} | + | public function __construct() { |
+ | $this-> | ||
+ | | ||
public function with(...$parameters) { | public function with(...$parameters) { | ||
Line 259: | Line 329: | ||
public function add(Point $point): Point { | public function add(Point $point): Point { | ||
return Point($this-> | return Point($this-> | ||
+ | } | ||
+ | | ||
+ | public function dot(Point $point): int { | ||
+ | return $this->x * $point-> | ||
} | } | ||
} | } | ||
- | interface Record | + | // used to enforce immutability but has nearly the same implementation |
- | public | + | readonly class Point { |
- | } | + | public |
- | // 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 __construct(public int $x, public int $y) {} | ||
Line 278: | Line 350: | ||
public function add(Point $point): Point { | public function add(Point $point): Point { | ||
return Point($this-> | return Point($this-> | ||
+ | } | ||
+ | | ||
+ | public function dot(Point $point): int { | ||
+ | return $this->x * $point-> | ||
} | } | ||
} | } | ||
Line 283: | Line 359: | ||
function Point(int $x, int $y): Point { | function Point(int $x, int $y): Point { | ||
static $points = []; | static $points = []; | ||
- | | + | |
- | $key = hash_func($x, $y); | + | $key = hash_object($mutablePoint); |
if ($points[$key] ?? null) { | if ($points[$key] ?? null) { | ||
// return an existing point | // return an existing point | ||
return $points[$key]; | return $points[$key]; | ||
} | } | ||
+ | | ||
// create a new point | // create a new point | ||
$reflector = new \ReflectionClass(Point_Implementation:: | $reflector = new \ReflectionClass(Point_Implementation:: | ||
- | $point = $reflector-> | + | $mutablePoint |
- | $point->x = $x; | + | $mutablePoint->x = $x; |
- | $point->y = $y; | + | $mutablePoint->y = $y; |
- | $point-> | + | $mutablePoint-> |
- | // copy properties to an immutable | + | |
- | $point = new Point($point->x, $point->y); | + | // copy properties to an immutable |
+ | $point = new Point($mutablePoint->x, $mutablePoint->y); | ||
+ | $point-> | ||
return $points[$key] = $point; | 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 ==== | ==== Performance considerations ==== | ||
- | To ensure that records are both performant and memory-efficient, | + | To ensure that records are both performant and memory-efficient, |
<code php> | <code php> | ||
- | $point1 = Point(3, 4); | + | $point1 = &Point(3, 4); |
$point2 = $point1; // No data duplication, | $point2 = $point1; // No data duplication, | ||
$point3 = Point(3, 4); // No data duplication, | $point3 = Point(3, 4); // No data duplication, | ||
$point4 = $point1-> | $point4 = $point1-> | ||
+ | $point5 = & | ||
</ | </ | ||
Line 320: | Line 399: | ||
Calling '' | Calling '' | ||
- | '' | + | If '' |
==== Serialization and deserialization ==== | ==== Serialization and deserialization ==== | ||
- | Records are fully serializable and deserializable. | + | Records are fully serializable and deserializable, even when nested. |
<code php> | <code php> | ||
Line 330: | Line 409: | ||
record Multiple(string $value1, string $value2); | record Multiple(string $value1, string $value2); | ||
- | echo $single = serialize(Single(' | + | echo $single = serialize(&Single(' |
- | echo $multiple = serialize(Multiple(' | + | echo $multiple = serialize(&Multiple(' |
- | echo unserialize($single) === Single(' | + | echo unserialize($single) === &Single(' |
- | echo unserialize($multiple) === Multiple(' | + | echo unserialize($multiple) === &Multiple(' |
</ | </ | ||
+ | |||
+ | If a record contains objects or values that are unserializable, | ||
==== Equality ==== | ==== Equality ==== | ||
Line 345: | Line 426: | ||
=== Non-trivial values === | === Non-trivial values === | ||
- | For non-trivial values (e.g., objects, closures, resources, etc.), the '' | + | For non-trivial values (e.g., objects, closures, resources, etc.), the '' |
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: | 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: | ||
Line 361: | Line 442: | ||
</ | </ | ||
- | 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) |
<code php> | <code php> | ||
Line 372: | Line 453: | ||
} | } | ||
- | $date1 = Date(' | + | $date1 = &Date(' |
- | $date2 = Date(' | + | $date2 = &Date(' |
- | echo $date1-> | + | echo $date1-> |
</ | </ | ||
Line 409: | Line 490: | ||
The '' | The '' | ||
- | Calling '' | + | Calling '' |
=== isRecord() === | === isRecord() === | ||
- | The '' | + | The '' |
=== getInlineConstructor() === | === getInlineConstructor() === | ||
The '' | The '' | ||
+ | |||
+ | Invoking the '' | ||
=== getTraditionalConstructor() === | === getTraditionalConstructor() === | ||
The '' | The '' | ||
+ | |||
+ | Invoking the '' | ||
=== makeMutable() === | === makeMutable() === | ||
Line 427: | Line 512: | ||
The '' | The '' | ||
- | A mutable record can be finalized again using '' | + | A mutable record can be finalized again using '' |
=== isMutable() === | === isMutable() === | ||
Line 440: | Line 525: | ||
record Seconds(int $seconds); | record Seconds(int $seconds); | ||
- | $example = Seconds(5); | + | $example = &Seconds(5); |
- | $reflector = new ReflectionRecord(ExpirationDate::class); | + | $reflector = new ReflectionRecord(Seconds::class); |
- | $expiration = $reflector-> | + | $expiration = $reflector-> |
$expiration-> | $expiration-> | ||
assert($example !== $expiration); | assert($example !== $expiration); | ||
Line 465: | Line 550: | ||
==== Considerations for implementations ==== | ==== Considerations for implementations ==== | ||
- | A '' | + | A '' |
==== Autoloading ==== | ==== 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. | + | Records will be autoloaded in the same way as classes. |
- | Once function autoloading is implemented in PHP at some hopeful point in the future, said autoloader could locate the record and then autoload it. | + | ==== New Functions ==== |
- | The author of this RFC strongly encourages someone to put forward a function autoloading RFC if autoloading is desired for records. | + | * '' |
===== Backward Incompatible Changes ===== | ===== Backward Incompatible Changes ===== | ||
To avoid conflicts with existing code, the '' | To avoid conflicts with existing code, the '' | ||
+ | |||
+ | Since '' | ||
===== Proposed PHP Version(s) ===== | ===== Proposed PHP Version(s) ===== | ||
Line 507: | Line 594: | ||
===== Open Issues ===== | ===== Open Issues ===== | ||
- | To-do | + | * Distill how CoW works, exactly. |
+ | * Address conflict with '' | ||
===== Unaffected PHP Functionality ===== | ===== Unaffected PHP Functionality ===== | ||
Line 516: | Line 604: | ||
* Records for " | * Records for " | ||
+ | * Short definition syntax for classes | ||
===== Proposed Voting Choices ===== | ===== 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 ===== | ===== Patches and Tests ===== | ||
Line 527: | Line 616: | ||
===== Implementation ===== | ===== Implementation ===== | ||
- | After the project is implemented, | + | To be completed during |
- | + | ||
- | - 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 ===== | ===== References ===== | ||
- | Links to external references, discussions or RFCs | + | * [[https:// |
===== Rejected Features ===== | ===== Rejected Features ===== | ||
- | Keep this updated with features that were discussed on the mail lists. | + | TBD |
rfc/records.1722624714.txt.gz · Last modified: 2024/08/02 18:51 by withinboredom