rfc:readonly_properties_v2

Differences

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

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
rfc:readonly_properties_v2 [2021/06/03 12:11]
nikic
rfc:readonly_properties_v2 [2021/07/20 15:37] (current)
nikic
Line 2: Line 2:
   * Date: 2021-06-02   * Date: 2021-06-02
   * Author: Nikita Popov <nikic@php.net>   * Author: Nikita Popov <nikic@php.net>
-  * Status: Draft+  * Status: Implemented
   * Target Version: PHP 8.1   * Target Version: PHP 8.1
   * Implementation: https://github.com/php/php-src/pull/7089   * Implementation: https://github.com/php/php-src/pull/7089
Line 8: Line 8:
 ===== Introduction ===== ===== Introduction =====
  
-This RFC introduces a ''readonly'' property modifier, which prevents modification of a properties after initialization.+This RFC introduces a ''readonly'' property modifier, which prevents modification of the property after initialization.
  
-Value objects are often immutable: Properties are initialized once in the constructor, and should not be modified afterwards. PHP currently has no way to enforce this constraint. The closest is to declare the property private, and only expose a getter publicly:+Value objects are often immutable: Properties are initialized once in the constructor, and should not be modified afterwards. PHP currently has no way to enforce this constraint. The closest alternative is to declare the property private, and only expose a public getter:
  
 <PHP> <PHP>
Line 24: Line 24:
 </PHP> </PHP>
  
-This doesn't actually make the property readonly, but at least the scope where modification could happen is tightened to a single class declaration, which is easier to audit.+This doesn't actually make the property readonly, but it does tighten the scope where modification could occur to a single class declaration. Unfortunately, this requires the use of getter boilerplate, which results in worse ergonomics for the consumer.
  
-Unfortunately, this forces you to use private properties together with public getter boilerplate, which makes for worse ergonomics for the consumer. Support for first-class readonly properties allows you to directly expose such properties, without fear:+Support for first-class readonly properties allows you to directly expose public readonly properties, without fear that class invariants could be broken through external modification:
  
 <PHP> <PHP>
Line 76: Line 76:
 <PHP> <PHP>
 class Test { class Test {
-    public readonly int $prop = 0;+    public function __construct( 
 +        public readonly int $= 0
 +        public readonly array $ary = [], 
 +    ) {}
 } }
  
 $test = new Test; $test = new Test;
-$test->prop += 1; +$test->+= 1; 
-$test->prop++; +$test->i++; 
-++$test->prop+++$test->i; 
-$ref =& $test->prop+$test->ary[] = 1; 
-$test->prop =& $ref; +$test->ary[0][] = 1
-byRef($test->prop);+$ref =& $test->i
 +$test->=& $ref; 
 +byRef($test->i);
 foreach ($test as &$prop); foreach ($test as &$prop);
 </PHP> </PHP>
Line 115: Line 120:
 </PHP> </PHP>
  
-The alternative would be to not use an implicit null default value for untyped readonly properties. However, this would be more confusing than simply making it an error condition, and letting the programmer explicitly opt-in by specifying the ''mixed'' type.+The alternative would be to not use an implicit null default value for untyped readonly properties. However, this would make the rules for implicit property default values more complex and confusing. Simply making it an error condition let'the programmer explicitly opt-in by specifying the ''mixed'' type.
  
-Specifying an explicitly default value on readonly properties is allowed. However, the default value is considered an initializing assignment, so that further modification is not possible:+Specifying an explicit default value on readonly properties is also not allowed:
  
 <PHP> <PHP>
 class Test { class Test {
 +    // Fatal error: Readonly property Test::$prop cannot have default value
     public readonly int $prop = 42;     public readonly int $prop = 42;
- 
-    public function __construct() { 
-        // Error, as property is already initialized. 
-        $this->prop = 24; 
-    } 
 } }
 </PHP> </PHP>
  
-This does not appear particularly useful at present, but could be more meaningful once objects are allowed as property initializersIn any case, the behavior in this case is clear and unambiguous, in that the programmer has gone out of their way to request a default value.+As the default value counts as an initializing assignment, a readonly property with a default value is essentially the same as a constant, and thus not particularly useful. The notion could become more useful in the future, if ''new'' expressions are allowed as property default valuesAt the same time, depending on how exactly property initialization would work in that case, having a default value on a readonly property could preclude userland serialization libraries from working, as they would not be able to replace the default-constructed object. Whether or not this is a concern depends on whether the property is initialized at time of object creation, or as an implicit part of the constructor (or similar). As these are open questions, the conservative choice is to forbid default values until these questions are resolved.
  
 It is worth reiterating here that default values on promoted parameters only apply to the parameter, not the property: It is worth reiterating here that default values on promoted parameters only apply to the parameter, not the property:
Line 152: Line 153:
 </PHP> </PHP>
  
-As the property has no default value, the assignment in the constructor is initializing, and thus legal. The constructor property promotion feature was specifically design for forward-compatiblity with readonly properties.+As the property has no default value, the assignment in the constructor is initializing, and thus legal. The constructor property promotion feature was specifically designed for forward-compatibility with readonly properties.
  
-Readonly static properties are not supported. This is a technical limitation, in that it is not possible to implement readonly static properties non-intrusively. In conjuction with the questionable usefulness of readonly static properties, this is not worthwhile at this time.+Readonly static properties are not supported. This is a technical limitation, in that it is not possible to implement readonly static properties non-intrusively. In conjunction with the questionable usefulness of readonly static properties, this is not considered worthwhile at this time.
  
 ==== Inheritance ==== ==== Inheritance ====
  
-It is not allows to override a read-write parent property with a readonly property in a child class:+It is not allowed to override a read-write property with a read-only property or vice versa. Both of the following are not legal:
  
 <PHP> <PHP>
Line 169: Line 170:
 } }
 </PHP> </PHP>
- 
-However, the converse is legal: 
  
 <PHP> <PHP>
Line 177: Line 176:
 } }
 class B extends A { class B extends A {
-    // Legal: readonly -> readwrite+    // Illegal: readonly -> readwrite
     public int $prop;     public int $prop;
 } }
 </PHP> </PHP>
  
-It is interesting to consider how property redeclaration interacts with the restriction that initializion can only occur in the declaring class:+It is obvious that overriding a readwrite property with a readonly property needs to be forbidden, because that may render operations performed in the parent class invalid. However, this proposal views readonly not just as a lack of capabilities (which would be safe to increase in a child class), but as an intentional restriction. Lifting the restriction in the child class could break invariants in the parent class. As such, a readonly modifier may be neither added nor removed during inheritance. 
 + 
 +It is interesting to consider how property redeclaration interacts with the restriction that initialization can only occur in the declaring class:
  
 <PHP> <PHP>
Line 195: Line 196:
 Here, initialization of ''B::$prop'' would be permitted both from inside ''A'' and ''B'', as both classes declare the property. A possible alternative would be to allow initialization only from ''B'', though that would mean that a redeclaration in a child class could break usage in a parent class. Here, initialization of ''B::$prop'' would be permitted both from inside ''A'' and ''B'', as both classes declare the property. A possible alternative would be to allow initialization only from ''B'', though that would mean that a redeclaration in a child class could break usage in a parent class.
  
-When the same property is imported from two traits, the ''readonly'' modifiers must match:+When the same property is imported from two traits, the ''readonly'' modifiers must also match:
  
 <PHP> <PHP>
Line 210: Line 211:
 </PHP> </PHP>
  
-One could argue that it should be possible to merge with a readonly and a readwrite property into a readwrite property. However, other modifiers currently also require strict equality, for example it is not possible to merge a public and a protected property. If these rules should be relaxed, they should be relaxed consistently. +Types on readonly properties remain invariant. One could argue that types of readonly properties could be covariant instead:
- +
-Types on readonly properties remain invariant. One could argue that types of readonly properties should be covariant instead:+
  
 <PHP> <PHP>
Line 223: Line 222:
 </PHP> </PHP>
  
-Covariance would hold for reads from the property, but not for the initializing assignment, which is permitted from both ''A'' and ''B'' here. I believe that covariance would only be truly correct if the parent property were abstract, which is currently not supported. A relaxation to covariant semantics would always be possible in the future.+Covariance would hold for reads from the property, but not for the initializing assignment, which is permitted from both ''A'' and ''B'' here. I believe that covariance would only be truly correct if the parent property were abstract, which is currently not supported. A relaxation to covariant semantics (either wholesale, or for abstract parents) would be possible in the future.
  
 ==== Unset ==== ==== Unset ====
Line 241: Line 240:
 </PHP> </PHP>
  
-However, it is possible to unset a readonly property prior to initialization, from the scope where the property has been declared. Just like with normal typed properties, explicitly unsetting the property makes it visibile to magic methods. In particular, this enabled the usual lazy initialization pattern to work:+However, it is possible to unset a readonly property prior to initialization, from the scope where the property has been declared. Just like with normal typed properties, explicitly unsetting the property makes it visible to magic methods. In particular, this enables the usual lazy initialization pattern to work:
  
 <PHP> <PHP>
Line 263: Line 262:
  
 A ''ReflectionProperty::isReadOnly()'' method is added, which reports whether a property is declared as read-only. ''ReflectionProperty::getModifiers()'' will also report a ''ReflectionProperty::IS_READONLY'' flag. A ''ReflectionProperty::isReadOnly()'' method is added, which reports whether a property is declared as read-only. ''ReflectionProperty::getModifiers()'' will also report a ''ReflectionProperty::IS_READONLY'' flag.
 +
 +''ReflectionProperty::setValue()'' can bypass the requirement that initialization occurs from the scope where the property has been declared. However, reflection cannot modify a readonly property that has already been initialized.
 +
 +Similarly, closure rebinding can be used to bypass the initialization scope requirement.
 +
 +==== Serialization ====
 +
 +Readonly properties have no impact on serialization. As ''%%__unserialize()%%'' (and the legacy ''Serializable::unserialize()'') method are invoked without a prior constructor call, readonly properties will be in an uninitialized state and can be set by the ''%%__unserialize()%%'' implementation.
 +
 +This also applies to userland serializers and hydrators. As long as the object is created using ''ReflectionClass::newInstanceWithoutConstructor()'' or some other constructor-bypass, it is always safe to initialize readonly properties.
  
 ===== Rationale ===== ===== Rationale =====
  
-The readonly property concept introduced in this proposal provides strong immutability guarantees, which apply both inside and outside the class. Once a property has been initialized, it cannot be changed under any circumstances.+The readonly property concept introduced in this proposal provides strong immutability guarantees, which apply both inside and outside the class. Once a property has been initialized, it cannot be changed under any circumstances. Reading a readonly property will always return the same value, no matter what code runs in between: 
 + 
 +<PHP> 
 +class Test { 
 +    public readonly string $prop; 
 +     
 +    public function method(Closure $fn) { 
 +        $prop = $this->prop; 
 +        $fn(); // Any code may run here. 
 +        $prop2 = $this->prop; 
 +        assert($prop === $prop2); // Always holds. 
 +    } 
 +
 +</PHP>
  
 These guarantees are //too// strong for certain use-cases. For example, some classes may wish to have properties that are publicly readable, but can only be written from within the class. This is a much weaker guarantee, as the value of a property can change during the lifetime of an object. //Both// variants can be useful depending on the situation, and the addition of readonly properties neither precludes nor discourages the addition of asymmetric property visibility. These guarantees are //too// strong for certain use-cases. For example, some classes may wish to have properties that are publicly readable, but can only be written from within the class. This is a much weaker guarantee, as the value of a property can change during the lifetime of an object. //Both// variants can be useful depending on the situation, and the addition of readonly properties neither precludes nor discourages the addition of asymmetric property visibility.
Line 294: Line 316:
 The clone-based implementation will result in an error, because the ''x'' property on the cloned object is already initialized, and modification is thus rejected. This is by design: The property does indeed get modified post-initialization, and the fact that this is only "temporary" is ultimately irrelevant. The clone-based implementation will result in an error, because the ''x'' property on the cloned object is already initialized, and modification is thus rejected. This is by design: The property does indeed get modified post-initialization, and the fact that this is only "temporary" is ultimately irrelevant.
  
-In the future, a [[https://externals.io/message/112624|clone with]] construct, which allows setting properties during the cloning process, would make such implementation compatible with readonly properties. The important difference is that the new value for the property is assigned directly, without assigning the old cloned value first.+In the future, a [[https://externals.io/message/112624|clone with]] construct, which allows setting properties during the cloning process, would make such implementations compatible with readonly properties. The important difference is that the new value for the property is assigned directly, without assigning the old cloned value first.
  
 Alternatively, such properties could use a future asymmetric visibility concept, though it does not express the actual invariant as precisely. Alternatively, such properties could use a future asymmetric visibility concept, though it does not express the actual invariant as precisely.
Line 304: Line 326:
 This also ensures that a potential future "clone with" implementation will only be able to modify readonly properties from private scope and thus cannot bypass additional invariants imposed by the implementation when used from a different scope. This also ensures that a potential future "clone with" implementation will only be able to modify readonly properties from private scope and thus cannot bypass additional invariants imposed by the implementation when used from a different scope.
  
-This RFC overlaps with the [[rfc:property_accessors|Property Accessors RFC]]. In particular, it implements the "only implicit get" aspect, though not with the exact same semantics. As mentioned on the RFC, I'm not convinced that the full complexity of accessors is truly warranted. Supporting readonly properties and asymmetric visibility would cover a signficant portion of the use-cases, at a lower language complexity cost.+This RFC overlaps with the [[rfc:property_accessors|Property Accessors RFC]]. In particular, it implements the "only implicit get" aspect, though not with the exact same semantics. As mentioned in the RFC, I'm not convinced that the full complexity of accessors is truly warranted. Supporting readonly properties and asymmetric visibility would cover a significant portion of the use-cases, at a lower language complexity cost.
  
-It is worth noting that having a readonly property feature does not preclude introduction of accessors. C# supports both readonly properties and accessors. C# provides properties with implicit backing storage through accessor syntax, but this is not the only way to do it. For example, Swift has special syntax for asymmetric visibility, rather than specifying visibility on implicitly implemented accessors.+It is worth noting that having a readonly property feature does not preclude introduction of accessors. C# supports both readonly properties and accessors. C# also provides properties with implicit backing storage through accessor syntax, but this is not the only way to do it. For example, Swift has special syntax for asymmetric visibility, rather than specifying visibility on implicitly implemented accessors.
  
-Even if we have property accessors, I believe it may be worthwhile to limit them to computed properties only, and solve use-cases that involve engine-managed storage through other mechanisms, such as readonly properties and property-level asymmetric visibility. This avoids confusion relating to the two kinds of accessors, and also allows us to make their behavior independent of accessor constraints. For example, a first-class asymmetric visibility feature would shield the user from considering distinctions such as ''get;'' vs ''&get;'' accessors. These are externalities of the general accessor feature and not needed for asymmetric visibility.+Even if we have property accessors, I believe it may be worthwhile to limit them to computed properties only, and solve use-cases that involve engine-managed storage through other mechanisms, such as readonly properties and property-level asymmetric visibility. This avoids confusion relating to the two kinds of accessors (implicit and explicit), and also allows us to make their behavior independent of accessor constraints. For example, a first-class asymmetric visibility feature would shield the user from considering distinctions such as ''get;'' vs ''&get;'' accessors. These are externalities of the general accessor feature and not needed for asymmetric visibility.
  
 A separate implementation can also be more efficient. After initialization, a readonly property will have the same performance characteristics as a normal property. Accessor-based properties, even with implicit storage, still carry a performance penalty. A separate implementation can also be more efficient. After initialization, a readonly property will have the same performance characteristics as a normal property. Accessor-based properties, even with implicit storage, still carry a performance penalty.
Line 320: Line 342:
 ===== Vote ===== ===== Vote =====
  
-Yes/No.+Voting started on 2021-07-01 and closes on 2021-07-15. 
 + 
 +<doodle title="Add readonly properties as proposed?" auth="nikic" voteType="single" closed="true"> 
 +   Yes 
 +   No 
 +</doodle>
  
rfc/readonly_properties_v2.1622722312.txt.gz · Last modified: 2021/06/03 12:11 by nikic