On Sat, Sep 10, 2022, at 4:34 PM, Nicolas Grekas wrote:
> Hello Larry,
>
>
>> Regarding your main question: I understand your problem with readonly
>> > classes, and I'd be happy if we found a solution which fits your
>> use-cases
>> > and keeps consistency for the engine at the same time. To give you more
>> > context about the inheritance related restriction in the RFC: I went with
>> > forbidding the extension of readonly classes by non-readonly ones so that
>> > the invariants of the parent (no dynamic or mutable properties are
>> allowed)
>> > don't occasionally get violated by the child. However, I cannot think
>> about
>> > a proper scenario now where this would cause any problems... I'm
>> wondering
>> > if anyone could come up with one?
>>
>> I don't have a real-world example, but a general pattern. The advantage
>> of a readonly object (either conventionally like PSR-7 or enforced by the
>> `readonly` keyword) is that you can store a reference to it without having
>> to worry about it changing out from under you. That means you can code on
>> the assumption that any data derived from that object is also not subject
>> to change.
>>
>> If you accept a readonly object of type Foo, you would write code around
>> it on that assumption.
>>
>> If you're then passed an object of type Bar extends Foo, which has an
>> extra non-readonly property, that assumption would now be broken. Whether
>> that counts as a Liskov violation is... a bit squishy, I think.
>>
>> Where that might be a problem is a case like this:
>>
>> readonly class Person {
>> public function __construct(public string $first, public string $last) {}
>>
>> public function fullName() {
>> return "$this->first $this->last";
>> }
>> }
>>
>> class FancyPerson extends Person {
>> public string $middle = '';
>>
>> public function setMiddle(string $mid) {
>> $this->middle = $mid;
>> }
>>
>> public function fullName() {
>> return "$this->first $this-middle $this->last";
>> }
>> }
>>
>> class Display {
>> public function __construct(protected Person $person) {}
>>
>> public function hello() {
>> return "Hello " . $this->person->fullName();
>> }
>> }
>>
>> Now, Display assumes Person is readonly, and thus fullName() is
>> idempotent, and thus Display::hello() is idempotent. But if you pass a
>> FancyPerson to Display, then fullName() is not safely idempotent (it may
>> change out from under you) and thus hello() is no longer idempotent.
>>
>> Whether or not that problem actually exists in practice is another
>> question. And to be fair, the same risk exists for conventionally-readonly
>> classes today (PSR-7, PSR-13, etc.), unless they're made final. Arguably
>> the safety signaling of a lexically readonly class is stronger than for a
>> conventionally readonly class, so one would expect that to not be broken.
>> But that's the case where a non-readonly child of a readonly parent can
>> cause problems.
>>
>
> Thanks for constructing this example, that's good food for thoughts.
> Unfortunately, The code following readonly child class shows that this
> safety you describe doesn't exist:
>
> readonly class FancyPerson extends Person {
> private readonly stdClass $middle;
>
> public function setMiddle(string $mid) {
> $this->middle ??= new stdClass;
> $this->middle->name = $mid;
> }
>
> public function fullName() {
> return "$this->first $this-middle $this->last";
> }
> }
Hm. I seem to recall during the discussion of readonly classes someone saying
that object properties of a readonly class had to also be readonly classes,
which would render the above code a compile error. However, I just checked and
that is not in the RFC. Was it removed? Am I imagining things? Anyone else
know what I'm talking about? :-)
--Larry Garfield
--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: https://www.php.net/unsub.php