Existe-t-il une stratégie de conception spécifique qui peut être appliquée pour résoudre la plupart des problèmes de poule et d'oeuf tout en utilisant des objets immuables?

17

Issu d'un background OOP (Java), j'apprends Scala par moi-même. Bien que je puisse facilement voir les avantages d'utiliser des objets immuables individuellement, j'ai du mal à voir comment on peut concevoir une application entière comme ça. Je vais donner un exemple:

Disons que j'ai des objets qui représentent des "matériaux" et leurs propriétés (je conçois un jeu, donc j'ai vraiment ce problème), comme l'eau et la glace. J'aurais un "gestionnaire" qui possède toutes ces instances de matériaux. Une propriété serait le point de congélation et de fusion, et ce à quoi le matériau gèle ou fond.

[EDIT] Toutes les instances matérielles sont "singleton", un peu comme une énumération Java.

Je veux que "l'eau" dise qu'elle gèle en "glace" à 0 ° C, et "glace" pour dire qu'elle fond en "eau" à 1 ° C. Mais si l'eau et la glace sont immuables, elles ne peuvent pas obtenir une référence l'une à l'autre en tant que paramètres de constructeur, car l'un d'entre eux doit être créé en premier, et que l'on ne peut pas obtenir de référence à l'autre en tant que paramètre de constructeur qui n'existe pas encore. Je pourrais résoudre ce problème en leur donnant à la fois une référence au gestionnaire afin qu'ils puissent l'interroger pour trouver l'autre instance de matériau dont ils ont besoin à chaque fois qu'on leur demande leurs propriétés de congélation / fusion, mais je reçois le même problème entre le gestionnaire et les matériaux, qu'ils ont besoin d'une référence les uns aux autres, mais il ne peut être fourni dans le constructeur que pour l'un d'eux, de sorte que le gestionnaire ou le matériel ne peut pas être immuable.

Est-ce qu'ils n'ont aucun moyen de contourner ce problème, ou dois-je utiliser des techniques de programmation "fonctionnelles", ou un autre modèle pour le résoudre?

Sébastien Diot
la source
2
pour moi, comme tu le dis, il n'y a ni eau ni glace. Il y a juste du h2omatériel
moucher
1
Je sais que cela aurait plus de "sens scientifique", mais dans un jeu, il est plus facile de le modéliser comme deux matériaux différents avec des propriétés "fixes", plutôt que comme un matériau avec des propriétés "variables" selon le contexte.
Sebastien Diot
Singleton est une idée stupide.
DeadMG
@DeadMG Eh bien, OK. Ce ne sont pas de vrais singletons Java. Je veux juste dire qu'il est inutile de créer plus d'une instance de chacun, car ils sont immuables et seraient égaux les uns aux autres. En fait, je n'aurai pas de véritables instances statiques. Mes objets "root" sont des services OSGi.
Sebastien Diot
La réponse à cette autre question semble confirmer mon soupçon que les choses se compliquent très rapidement avec les immuables: programmers.stackexchange.com/questions/68058/…
Sebastien Diot

Réponses:

2

La solution est de tricher un peu. Plus précisément:

  • Créez A, mais laissez sa référence à B non initialisée (car B n'existe pas encore).

  • Créez B et faites-le pointer sur A.

  • Mettez à jour A pour pointer vers B. Ne mettez pas à jour A ou B après cela.

Cela peut être fait explicitement (exemple en C ++):

struct List {
    int n;
    List *next;

    List(int n, List *next)
        : n(n), next(next);
};

// Return a list containing [0,1,0,1,...].
List *binary(void)
{
    List *a = new List(0, NULL);
    List *b = new List(1, a);
    a->next = b; // Evil, but necessary.
    return a;
}

ou implicitement (exemple dans Haskell):

binary :: [Int]
binary = a where
    a = 0 : b
    b = 1 : a

L'exemple Haskell utilise une évaluation paresseuse pour obtenir l'illusion de valeurs immuables mutuellement dépendantes. Les valeurs commencent par:

a = 0 : <thunk>
b = 1 : a

aet bsont tous deux des formes valides normales à la tête indépendamment. Chaque inconvénient peut être construit sans avoir besoin de la valeur finale de l'autre variable. Lorsque le thunk est évalué, il pointera alors vers les mêmes donnéesb points de vers.

Ainsi, si vous souhaitez que deux valeurs immuables pointent l'une vers l'autre, vous devez soit mettre à jour la première après avoir construit la seconde, soit utiliser un mécanisme de niveau supérieur pour faire de même.


Dans votre exemple particulier, je pourrais l'exprimer dans Haskell comme suit:

data Material = Water {temperature :: Double}
              | Ice   {temperature :: Double}

setTemperature :: Double -> Material -> Material
setTemperature newTemp (Water _) | newTemp <= 0.0 = Ice newTemp
                                 | otherwise      = Water newTemp
setTemperature newTemp (Ice _)   | newTemp >= 1.0 = Water newTemp
                                 | otherwise      = Ice newTemp

Cependant, je contourne la question. J'imagine que dans une approche orientée objet, où une setTemperatureméthode est attachée au résultat de chaque Materialconstructeur, il faudrait que les constructeurs pointent les uns vers les autres. Si les constructeurs sont traités comme des valeurs immuables, vous pouvez utiliser l'approche décrite ci-dessus.

Joey Adams
la source
(en supposant que j'ai compris la syntaxe de Haskell) Je pense que ma solution actuelle est en fait très similaire, mais je me demandais si c'était la "bonne", ou si quelque chose de mieux existe. Je crée d'abord une "poignée" (référence) pour chaque objet (pas encore créé), puis je crée tous les objets, en leur donnant les poignées dont ils ont besoin, et finalement initialise la poignée aux objets. Les objets sont eux-mêmes immuables, mais pas les poignées.
Sebastien Diot
6

Dans votre exemple, vous appliquez une transformation à un objet, donc j'utiliserais quelque chose comme une ApplyTransform()méthode qui renvoie unBlockBase plutôt que d'essayer de changer l'objet actuel.

Par exemple, pour changer un IceBlock en un WaterBlock en appliquant un peu de chaleur, j'appellerais quelque chose comme

BlockBase currentBlock = new IceBlock();
currentBlock = currentBlock.ApplyTemperature(1); 
// currentBlock is now a WaterBlock 

et la IceBlock.ApplyTemperature()méthode ressemblerait à ceci:

public class IceBlock() : BlockBase
{
    public BlockBase ApplyTemperature(int temp)
    {
        return (temp > 0 ? new WaterBlock((BlockBase)this) : this);
    }
}
Rachel
la source
C'est une bonne réponse, mais malheureusement uniquement parce que j'ai omis de mentionner que mes "matériaux", en fait mes "blocs", sont tous singleton, donc le nouveau WaterBlock () n'est tout simplement pas une option. C'est le principal avantage de l'immuable, vous pouvez les réutiliser à l'infini. Au lieu d'avoir 500 000 blocs en RAM, j'ai 500 000 références à 100 blocs. Beaucoup moins cher!
Sebastien Diot
Alors qu'en est-il du retour BlockList.WaterBlockau lieu de créer un nouveau bloc?
Rachel
Oui, c'est ce que je fais, mais comment obtenir une liste de blocage? Evidemment, les blocs doivent être créés avant la liste des blocs, et donc, si le bloc est vraiment immuable, il ne peut pas recevoir la liste des blocs en paramètre. Alors d'où vient la liste? Mon point général est que, en rendant le code plus compliqué, vous résolvez le problème de la poule et de l'œuf à un niveau, juste pour le récupérer à l'autre. Fondamentalement, je ne vois aucun moyen de créer une application entière basée sur l'immuabilité. Elle ne semble applicable qu'aux "petits objets", mais pas aux conteneurs.
Sebastien Diot
@Sebastien Je pense que BlockListc'est juste une staticclasse qui est responsable des instances uniques de chaque bloc, donc vous n'avez pas besoin de créer une instance de BlockList(j'ai l'habitude de C #)
Rachel
@Sebastien: Si vous utilisez des Singeltons, vous en payez le prix.
DeadMG
6

Une autre façon de briser le cycle est de séparer les préoccupations du matériel et de la transmutation, dans un langage inventé:

water = new Block("water");
ice = new Block("ice");

transitions = new Transitions([
    new transitions.temperature.Below(0.0, water, ice),
    new transitions.temperature.Above(0.0, ice, water),
]);
SingleNegationElimination
la source
Huh, c'était difficile pour moi de lire celui-ci au début, mais je pense que c'est essentiellement la même approche que je préconisais.
Aidan Cully
1

Si vous allez utiliser un langage fonctionnel et que vous souhaitez réaliser les avantages de l'immuabilité, vous devez aborder le problème en gardant cela à l'esprit. Vous essayez de définir un type d'objet "glace" ou "eau" qui peut supporter une plage de températures - afin de supporter l'immuabilité, vous devez ensuite créer un nouvel objet à chaque changement de température, ce qui est un gaspillage. Essayez donc de rendre les concepts de type de bloc et de température plus indépendants. Je ne connais pas Scala (c'est sur ma liste à apprendre :-)), mais en empruntant à Joey Adams Answer in Haskell , je suggère quelque chose comme:

data Material = Water | Ice

blockForTemperature :: Double -> Material
blockForTemperature x = 
  if x < 0 then Ice else Water

ou peut-être:

transitionForTemperature :: Material -> Double -> Material
transitionForTemperature oldMaterial newTemp = 
  case (oldMaterial, newTemp) of
    (Ice, _) | newTemp > 0 -> Water
    (Water, _) | newTemp <= 0 -> Ice

(Remarque: je n'ai pas essayé de faire ça, et mon Haskell est un peu rouillé.) Maintenant, la logique de transition est séparée du type de matériau, donc ça ne gaspille pas autant de mémoire, et (à mon avis) c'est assez un peu plus fonctionnellement orienté.

Aidan Cully
la source
Je n'essaie pas vraiment d'utiliser le "langage fonctionnel" parce que je ne comprends pas! La seule chose que je retiens normalement de tout exemple de programmation fonctionnelle non triviale est: "Merde, moi qui étais plus intelligent!" C'est juste au-delà de moi comment cela peut avoir du sens pour n'importe qui. Depuis mes jours d'étudiants, je me souviens que Prolog (basé sur la logique), Occam (tout fonctionne en parallèle par défaut) et même l'assembleur avaient du sens, mais Lisp était juste un truc de fou. Mais je vous fais bien de déplacer le code qui provoque un "changement d'état" en dehors de l '"état".
Sebastien Diot