rfc:records

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
rfc:records [2024/08/02 18:51] – add complex types withinboredomrfc: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, <landers.robert@gmail.com> +  * 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: http://wiki.php.net/rfc/records   * First Published at: http://wiki.php.net/rfc/records
  
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 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: 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 ''%%::%%'' 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.
 +
 +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.
  
 <code php> <code php>
Line 126: Line 128:
 === Usage === === Usage ===
  
-A record may be used as 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
 + 
 +<code php> 
 +$gray = $bucket->mixIn($blackPaint)->mixIn($whitePaint); 
 +</code> 
 + 
 +Records are instantiated in a function format, with ''%%&%%'' prependedThis provides visual feedback that a record is being created instead of a function call. 
 + 
 +<code 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 
 +</code>
  
 === 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)); // output a record with x: 10 and y: 10+var_dump(&Rectangle(10)); // output a record with x: 10 and y: 10
 </code> </code>
  
Line 164: Line 185:
 } }
  
-$userId = UserId(1);+$userId = &UserId(1);
 $otherId = $userId->with(2); // Fails: Named arguments must be used $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(serialNumber: "U2"); // Error: serialNumber is not defined in the inline constructor
Line 175: Line 196:
 record Vector(int $dimensions, int ...$values); 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); // 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, 1, 2, 3, 4); // Error: mixing named arguments with variadic arguments is not allowed by PHP syntax
Line 183: Line 204:
 == Custom with method == == 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. +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.
- +
-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.+
  
 <code php> <code php>
Line 213: Line 229:
  
 <code php> <code php>
-// Inline constructor +// Inline constructor defining two properties 
-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->emailAddress)) {
       throw new InvalidArgumentException("Invalid email address");       throw new InvalidArgumentException("Invalid email address");
     }     }
          
-    $this->id = hash('sha256', $email); +    $this->id = hash('sha256', $this->emailAddress); 
-    $this->name = ucwords($name);+    $this->name = ucwords($this->name); 
 +    // all properties are now immutable
   }   }
 +}
 +</code>
 +
 +==== 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 "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;
 } }
 </code> </code>
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 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: 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->magnitude = sqrt($this->x ** 2 + $this->y ** 2);
 +    }
 +
     public function add(Point $point): Point {     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;
     }     }
 } }
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->magnitude = sqrt($this->x ** 2 + $this->y ** 2); 
 +    }
  
     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->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;
     }     }
 } }
  
-interface Record +// used to enforce immutability but has nearly the same implementation 
-    public function with(...$parameters): self; +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 __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->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;
     }     }
 } }
Line 283: Line 359:
 function Point(int $x, int $y): Point { function Point(int $x, int $y): Point {
     static $points = [];     static $points = [];
-    // look up the identity of the point +     
-    $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::class);     $reflector = new \ReflectionClass(Point_Implementation::class);
-    $point = $reflector->newInstanceWithoutConstructor(); +    $mutablePoint = $reflector->newInstanceWithoutConstructor(); 
-    $point->x = $x; +    $mutablePoint->x = $x; 
-    $point->y = $y; +    $mutablePoint->y = $y; 
-    $point->__construct(); +    $mutablePoint->__construct(); 
-    // copy properties to an immutable point and return it +     
-    $point = new Point($point->x, $point->y);+    // copy properties to an immutable Point and return it 
 +    $point = new Point($mutablePoint->x, $mutablePoint->y)
 +    $point->magnitude = $mutablePoint->magnitude;
     return $points[$key] = $point;     return $points[$key] = $point;
 } }
 </code> </code>
  
-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, 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.
  
 <code php> <code php>
-$point1 = Point(3, 4);+$point1 = &Point(3, 4);
 $point2 = $point1; // No data duplication, $point2 references the same data as $point1 $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 $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 $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
 </code> </code>
  
Line 320: Line 399:
 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%%''. 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 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 mistake.
  
 ==== 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('value')); // Outputs: "O:6:"Single":1:{s:5:"value";s:5:"value";}" +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 $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($single) === &Single('value'); // Outputs: true 
-echo unserialize($multiple) === Multiple('value1', 'value2'); // Outputs: true+echo unserialize($multiple) === &Multiple('value1', 'value2'); // Outputs: true
 </code> </code>
 +
 +If a record contains objects or values that are unserializable, the record will not be serializable.
  
 ==== Equality ==== ==== Equality ====
Line 345: Line 426:
 === Non-trivial values === === 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: 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:
 </code> </code>
  
-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:
  
 <code php> <code php>
Line 372: Line 453:
 } }
  
-$date1 = Date('2024-07-19'); +$date1 = &Date('2024-07-19'); 
-$date2 = 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
 </code> </code>
  
Line 409: Line 490:
 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. 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() === === 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() === === 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. 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() === === 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. 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() === === makeMutable() ===
Line 427: Line 512:
 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. 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() === === 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->newInstanceWithoutConstructor();+$expiration = $reflector->newInstanceWithoutConstructor(); // this is a mutable object
 $expiration->seconds = 5; $expiration->seconds = 5;
 assert($example !== $expiration); // true assert($example !== $expiration); // true
Line 465: Line 550:
 ==== Considerations for implementations ==== ==== 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 ''%%function%%'' with the same name.+A ''%%record%%'' cannot share its name with an existing ''%%record%%'', ''%%class%%'', ''%%interface%%''''%%trait%%'', or ''%%function%%'', just like class.
  
 ==== 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.+  * ''%%record_exists%%'' will return ''%%true%%'' if a record exists and ''%%false%%'' otherwise. It has the same signature as ''%%class_exists%%''.
  
 ===== Backward Incompatible Changes ===== ===== Backward Incompatible Changes =====
  
 To avoid conflicts with existing code, the ''%%record%%'' keyword will be handled similarly to ''%%enum%%'' to prevent backward compatibility issues. 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) ===== ===== Proposed PHP Version(s) =====
Line 507: Line 594:
 ===== Open Issues ===== ===== Open Issues =====
  
-To-do+  * Distill how CoW works, exactly. 
 +  * Address conflict with ''%%&%%'' syntax: https://3v4l.org/CE5rt
  
 ===== Unaffected PHP Functionality ===== ===== Unaffected PHP Functionality =====
Line 516: Line 604:
  
   * Records for "record-like" types, such as DateTime, DateInterval, and others.   * Records for "record-like" types, such as DateTime, DateInterval, and others.
 +  * 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, this section should contain +To be completed during later phase of discussion.
- +
-  - the version(s) it was merged into +
-  - 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://en.wikipedia.org/wiki/Value_semantics|Value semantics]]
  
 ===== 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