1. Immutable vs Mutable


1.1. Email de prérequis

Voici l'email de prérequis.
https://marc.info/?l=php-internals&m=160935644205607&w=2

List: php-internals
Subject: =?UTF-8?Q?Re:_[PHP-DEV]_Analysis_of_property_visibility,_immutability,_a?= =?UTF-8?Q?nd_cloning_prop
From: "Larry Garfield" <larry () garfieldtech ! com>
Date: 2020-12-30 19:26:25
Message-ID: b3fba6e2-8547-4de6-a09f-bd980b24a97a () www ! fastmail ! com
[Download RAW message or body]


> > That's a good summary of why immutability and with-er methods (or some
> > equivalent) are more ergonomic.
> >
> > Another point to remember: Because of PHP's copy-on-write behavior, full on
> > immutability doesn't actually waste that much memory. It does use up some,
> > but far less than you think. (Again, based on the tests MWOP ran for PSR-7
> > a ways back.)
>
> I thought copy-on-write was only for arrays, not objects?
>
> Olle

Copy on write applies to all values; the caveat is that with objects, the value being \
copied is the handle that points to an object in memory, rather than the object \
itself. That means passing an object by reference can do some seriously unexpected \
things, which is why you basically never do so.

The point here is that if you have an object with 15 internal properties, it's memory \
usage is 15 zvals plus one zval for the object, plus one zval for the variable that \
points to it. (I'm over-simplifying here. A lot.) If you pass it to a function, \
only the one zval for the handle is duplicated, which is the same as for an integer.

If you clone the object, you don't duplicate 15+1 zvals. You duplicate just the one \
zval for the object itself, which reuses the existing 15 internal property entries. \
If in the new object you then update just the third one, PHP then duplicates just \
that one internal zval and modifies the new one. So you still are using only 18 \
zvals, not 36 zvals. (Engine people: Yes, I am *very* over-simplifying. I know.)

Basically, what in most languages would require manually implementing "immutable data \
structures" we get for free in PHP, which is seriously sweet.

The net result is that a with-er chain like this:

$foo2 = $foo->withBar('x')->withBaz('y')->withBeep('z');

is way, way less expensive than it looks, both on memory and CPU. It is more \
expensive than setters, but not by much.

That's why I don't think the distinction between unique and immutable mentioned \
up-thread is that big of a deal in PHP, specifically. Yes, they're different things, \
but the cost of them is not all that different because of CoW, so considering them \
separately is not as important as it would be in a language that doesn't \
automatically do CoW in the background for us.

(Whoever in the 90s decided to bake CoW into the engine, thank you. It's an \
incredibly nice foundational feature.)

--Larry Garfield

--
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: https://www.php.net/unsub.php

1.2. Ce que j'en comprends

J'en comprends que si on construit des clones d'instances à chaque withXxx plutôt que de faire un clone suivi de setter, même si selon l'auteur, cela consomme un peu plus de mémoire/CPU mais c'est négligeable comparé à l'intéret que c'est de travailler avec des immutables.

En bref, comparer ceci :

<?php
$foo = new Foo();
$foo2 = $foo->withBar('x')->withBaz('y')->withBeep('z');

Avec ceci :

<?php
$foo = new Foo();
$foo2 = clone $foo;
$foo2 = $foo2->setBar('x')->setBaz('y')->setBeep('z');

Cette deuxième version, pour un même résultat, serait plus gourmande en mémoire et en CPU que la première version.

1.3. On teste !

1.3.1 Classe Foo.

Bon, ce qui est bien, c'est que l'on peut définir la même classe Foo pour les deux façons de faire.

<?php
class Foo
{
    protected string $bar = 'a';
    protected string $baz = 'b';
    protected string $beep = 'c';

    public function __construct()
    {
        return $this;
    }

    public function setBar(string $bar)
    {
        $this->bar = $bar;

        return $this;
    }

    public function setBaz(string $baz)
    {
        $this->baz = $baz;

        return $this;
    }

    public function setBeep(string $beep)
    {
        $this->beep = $beep;

        return $this;
    }

    public function withBar(string $bar)
    {
        $clonedFoo = clone $this;
        $clonedFoo->bar = $bar;

        return $clonedFoo;
    }
    public function withBaz(string $baz)
    {
        $clonedFoo = clone $this;
        $clonedFoo->baz = $baz;

        return $clonedFoo;
    }
    public function withBeep(string $beep)
    {
        $clonedFoo = clone $this;
        $clonedFoo->beep = $beep;

        return $clonedFoo;
    }
}


1.3.2 Test de l'usage mémoire.

Pour calculer l'usage qu'est faite de la mémoire, on peut utiliser la fonction PHP memory_get_usage à plusieurs endroits du code et faire la différence.

$m1 =  memory_get_usage();
$foo = new Foo();
$foo2 = $foo->withBar('x')->withBaz('y')->withBeep('z');
$m2 = memory_get_usage();
echo ($m2 - $m1).PHP_EOL;


$m1 =  memory_get_usage();
$foo3 = new Foo();
$foo4 = clone $foo3;
$foo4 = $foo4->setBar('x')->setBaz('y')->setBeep('z');
$m2 = memory_get_usage();
echo ($m2 - $m1).PHP_EOL;


Le résultat que j'ai obtenu est celui-ci :

192
192

Autrement dit : aucune différence. Ce qui amène plusieurs hypothèses :
- soit je n'ai pas compris ce dont il était question ;
- soit j'ai mal implémenté le truc ;
- soit memory_get_usage ne fait pas ce que je souhaite ;
- ou soit il n'y a effectivement aucune différence de mémoire, en tout cas au final, peut-être qu'à chaque étape intermédiaire, si ;

En testant la dernière hypothèse, et donc en regardant la mémoire utilisée maximale sur chaque cas entre chaque étape, j'obtiens :
Cas 1 :

<?php
$max1 = 0;
$max1 = max($max1, memory_get_usage());
$foo = new Foo();
$max1 = max($max1, memory_get_usage());
$foo2 = $foo->withBar('x');
$max1 = max($max1, memory_get_usage());
$foo2 = $foo2->withBaz('y');
$max1 = max($max1, memory_get_usage());
$foo2 = $foo2->withBeep('z');
$max1 = max($max1, memory_get_usage());
echo ($max1).PHP_EOL;

Cas 2 :

$max1 = 0;
$max1 = max($max1, memory_get_usage());
$foo3 = new Foo();
$max1 = max($max1, memory_get_usage());
$foo4 = clone $foo3;
$max1 = max($max1, memory_get_usage());
$foo4 = $foo4->setBar('x');
$max1 = max($max1, memory_get_usage());
$foo4 = $foo4->setBaz('y');
$max1 = max($max1, memory_get_usage());
$foo4 = $foo4->setBeep('z');
$max1 = max($max1, memory_get_usage());
echo ($max1).PHP_EOL;

Cas 1 : 406464
Cas 2 : 406488

Soit une différence de 14 octets, le deuxième cas étant donc à un moment plus gourmand en mémoire que le premier cas.

1.3.3 Autres benchmarks

$foo = new Foo();
$foo2 = $foo->withBar('x')->withBaz('y')->withBeep('z');

-> 404680

$foo = new Foo();
$foo2 = $foo->withBar('x');
$foo2 = $foo2->withBaz('y');
$foo2 = $foo2->withBeep('z');

-> 404808

$foo3 = new Foo();
$foo4 = clone $foo3;
$foo4 = $foo4->setBar('x');
$foo4 = $foo4->setBaz('y');
$foo4 = $foo4->setBeep('z');

-> 404832

$foo3 = new Foo();
$foo4 = clone $foo3;
$foo4 = $foo4->setBar('x')->setBaz('y')->setBeep('z');

-> 404832

Analyse : La version immutable où les withXxx sont chainés consomme moins de mémoire que les autres versions dont celle mutable avec les setXxx chainés, donc ce code-ci n'est pas une preuve de ce que dit l'auteur du mail, ce serait même l'inverse.

1.4. Conclusion

Bien que j'ai n'a pas réussi à montrer que la mémoire utilisée est plus grande lorsqu'on manipule des objets immutables, et c'est même l'inverse dans mon cas, la version avec des withXxx parait être très bien gérée par PHP et donc on peut imaginer que coder avec des objets immutables serait très souhaité.

2. Qui est le premier, l'oeuf ou la poule ?


Considérons deux classes, Hen et Egg avec ces deux règles :
* Lorsqu'une poule pond, elle crée un oeuf.
* Lorsqu'un oeuf éclot, une poule nait.

<?php
class Hen
{
    public function lay()
    {
        return new Egg();
    }
}

class Egg
{
    public function hatch()
    {
        return new Hen();
    }
}


On peut donc créer des petits enfants très simplement :

$hen = (new Hen())->lay()->hatch();


Ce design pattern est à ranger dans la catégorie des Constructors (ce n'est qu'un double FactoryMethod).

3. Quand le \n fait un u/ puis un \n


Un petit bout de code juste pour le fun. On va écrire la fonction br2nl, après tout il n'y a pas de raison qu'on puisse aller dans le sens retour à la ligne PHP-> retour à la ligne HTML et pas dans le sens inverse.

C'est parti donc, ! Attention cela risque d'être intense (note : je ne prends pas en compte le xHTML, faut pas déconner).

function br2nl(string $string)
{
    return str_replace('<br>', PHP_EOL, $string);
}

et vient alors naturellement les fonctions suivantes :

function saltoAvant(string $string) 
{
    return br2nl(nl2br($string));
}

function saltoArriere(string $string)
{
    return nl2br(br2nl($string));
}


On sait jamais si le \n change d'avis une fois sur le u/, cela peut être pratique !

4. Un Echo lointain franchit les âges


Un code artistique :

<?php
echo 'echo';

Le résultat est bien sûr :

echo


je trouve cela très beau.


5. Le lcwords


Qu'on résume :
* lcfirst, c'est pour mettre la première lettre d'une chaine en minuscule
* ucfirst, c'est pour mettre la première lettre d'une chaine en majuscule
* ucwords, c'est pour mettre la première lettre de chaque mot d'une chaine en majuscule

lc voulant dire lower case et uc voulant dire upper case.

Quant à words, cela veut dire 'mots', et bien sûr first, c'est pour dire que c'est la première lettre, encore une étrangeté sur le nommage des fonctions et la cohérence de tout ça en PHP (<8.0).
Mais admettons ! La question que je me pose est, "mais pourquoi donc lcfirst existe ?".

Après une rapide recherche sur Github, c'est édifiant :
* lcfirst a été surtout utilisé dans des tests unitaires...
* lcfirst a été utilisé pour générer du code php (ex: générer "$toto = new Toto()" -> on génère le nom de l'instance en fonction de celui de la classe, pareil c'est surement pour faire des tests mais plus surement utiles cette fois, ex2: générer un namespace en fonction du nom du fichier)

je n'ai à ce jour pas d'idée fonctionnelle de l'intérêt de cette fonction mais au moins elle a été utilisée.

Par contre, mais où est la fonction lcwords ?!! Qu'à cela ne tienne, en voici une implémentation :

function lcwords(string $string): string
{
    return implode(' ', array_map(function(string $piece) {
            return lcfirst($piece);
        }, explode(' ', $string)
    ));
}

echo lcwords('THE CAKE IS A LIE');


Le résultat est sans appel :

>tHE cAKE iS a lIE

Chaque première lettre des mots est bien en minuscule, super donc !

Evidemment le nom de la fonction est dans l'esprit du nommage des autres.