Dans Mathematica, la plupart de vos actions sont basées sur des expressions. Les expressions ont naturellement une structure arborescente. Pour les traversées en profondeur (qui sont probablement les plus courantes), vous pouvez alors utiliser des fonctions telles que Scan
, Map
, Cases
etc. La différence par rapport aux langages plus traditionnels est qu'il n'existe pas de moyen simple de préserver l'identité d'un nœud individuel dans un arbre d'expression, puisqu'il n'y a pas de pointeurs en Mathematica. En outre, de nombreuses opérations sur les expressions qui sont idiomatiques en Mathematica copieraient l'expression entière alors que vous n'avez besoin de la modifier qu'à quelques endroits, car les expressions sont immuables.
L'utilisation d'expressions Mathematica immuables sous forme d'arbres présente encore plusieurs avantages. L'un d'eux est que, comme elles sont immuables, il est facile de comprendre ce qu'elles stockent en les regardant simplement (l'état et le comportement ne sont pas mélangés). Un autre avantage est qu'il existe des fonctions efficaces et génériques telles que Map
, MapIndexed
ou Scan
qui les traversent. Par exemple, le modèle de conception visiteur est invisible - c'est juste Map[f,tree,Infinity]
intégrés dans la langue. Il existe également des fonctions intégrées telles que Cases
, Replace
, ReplaceAll
etc., qui permettent d'écrire un code très concis et déclaratif pour déstructurer les arbres, trouver des morceaux d'arbres avec une certaine syntaxe ou satisfaisant une certaine condition, etc. Puisque les arbres ne sont pas limités à être construits uniquement à partir de listes et à être construits à partir de différentes têtes, on peut effectivement utiliser ceci pour écrire un code de traitement d'arbre très concis. Enfin, la liberté de construire très facilement n'importe quelle structure d'arbre que vous voulez rend beaucoup plus facile la réalisation d'expériences et de prototypes, dans l'esprit de la programmation exploratoire et ascendante ce qui permet de raccourcir le cycle de développement et, en fin de compte, d'obtenir de meilleures conceptions.
Cela dit, vous pouvez certainement mettre en œuvre une structure de données arborescente "étatique" (mutable). La véritable raison pour laquelle cela n'a pas encore été fait est, je pense, le manque de performance associé à la construction, la modification et la traversée d'un tel arbre, puisqu'il subira un processus d'évaluation symbolique complet à chaque étape (cf. ce pour plus de détails à ce sujet). Pour 2 exemples de la façon dont, par exemple, un arbre de recherche binaire peut être utilisé dans le contexte de Mathematica pour un code assez efficace, voir mes posts ici (cadre symbolique générique) et ici (dans le contexte du code compilé). Pour les moyens généraux de construire des structures de données de manière idiomatique dans Mathematica, je recommande les livres de Roman Maeder : "Programming in Mathematica", "Mathematica programmer I&II", et surtout "Computer Science in Mathematica". Dans ce dernier, il explique en détail comment implémenter un arbre de recherche binaire en Mathematica. EDIT Comme @Simon l'a mentionné, la conférence de @Daniel Lichtblau est également une excellente ressource, qui montre comment construire des structures de données et les rendre efficaces.
En ce qui concerne les moyens généraux d'implémenter des structures de données dans Mathematica qui incorporeraient un certain état, voici un exemple simple extrait de mon post dans ce Le fil Mathgroup - il met en œuvre une structure de données "paire".
Unprotect[pair, setFirst, getFirst, setSecond, getSecond, new, delete];
ClearAll[pair, setFirst, getFirst, setSecond, getSecond, new, delete];
Module[{first, second},
first[_] := {};
second[_] := {};
pair /: new[pair[]] := pair[Unique[]];
pair /: pair[tag_].delete[] := (first[tag] =.; second[tag] =.);
pair /: pair[tag_].setFirst[value_] := first[tag] = value;
pair /: pair[tag_].getFirst[] := first[tag];
pair /: pair[tag_].setSecond[value_] := second[tag] = value;
pair /: pair[tag_].getSecond[] := second[tag];
Format[pair[x_Symbol]] := "pair[" <> ToString[Hash[x]] <> "]";
];
Protect[pair, setFirst, getFirst, setSecond, getSecond, new, delete];
Voici comment vous pourriez l'utiliser :
pr = new[pair[]];
pr.setFirst[10];
pr.setSecond[20];
{pr.getFirst[], pr.getSecond[]}
{10, 20}
Création d'une liste de nouveaux objets de paire :
pairs = Table[new[pair[]], {10}]
{"pair[430427975]", "pair[430428059]", "pair[430428060]", "pair[430428057]",
"pair[430428058]", "pair[430428063]", "pair[430428064]", "pair[430428061]",
"pair[430428062]", "pair[430428051]"}
Définition des champs :
Module[{i},
For[i = 1, i <= 10, i++,
pairs[[i]].setFirst[10*i];
pairs[[i]].setSecond[20*i];]]
Vérification des champs :
#.getFirst[] & /@ pairs
{10, 20, 30, 40, 50, 60, 70, 80, 90, 100}
#.getSecond[] & /@ pairs
{20, 40, 60, 80, 100, 120, 140, 160, 180, 200}
Dans le post que j'ai mentionné, il y a une discussion plus détaillée. Un gros problème pour les "objets" créés de cette manière est qu'il n'y a pas de collecte automatique des déchets pour eux, ce qui peut être l'une des principales raisons pour lesquelles les extensions de la POO mises en œuvre dans Mathematica de haut niveau n'ont pas vraiment décollé.
Il existe plusieurs extensions de la POO pour Mathematica, telles que l'application classes.m
par Roman Maeder (la source se trouve dans son livre "Mathematica Programmer"), le paquet Objectica
paquet commercial, et plusieurs autres. Mais jusqu'à ce que Mathematica lui-même fournisse des mécanismes efficaces (peut-être basés sur une sorte de pointeur ou de mécanisme de référence) pour construire des structures de données mutables (si cela se produit un jour), il y aura probablement un gros problème de performance associé aux implémentations de haut niveau de telles structures de données dans mma. De plus, comme mma est basé sur l'immuabilité comme l'une des idées principales, il n'est pas très facile de faire en sorte que les structures de données mutables s'adaptent bien aux autres idiomes de la programmation Mathematica.
EDIT
Voici une mise en œuvre d'un arbre à état simple selon les lignes de l'exemple ci-dessus :
Module[{parent, children, value},
children[_] := {};
value[_] := Null;
node /: new[node[]] := node[Unique[]];
node /: node[tag_].getChildren[] := children[tag];
node /: node[tag_].addChild[child_node, index_] :=
children[tag] = Insert[children[tag], child, index];
node /: node[tag_].removeChild[index_] :=
children[tag] = Delete[children[tag], index];
node /: node[tag_].getChild[index_] := children[tag][[index]];
node /: node[tag_].getValue[] := value[tag];
node /: node[tag_].setValue[val_] := value[tag] = val;
];
Quelques exemples d'utilisation :
In[68]:= root = new[node[]]
Out[68]= node[$7]
In[69]:= root.addChild[new[node[]], 1]
Out[69]= {node[$8]}
In[70]:= root.addChild[new[node[]], 2]
Out[70]= {node[$8], node[$9]}
In[71]:= root.getChild[1].addChild[new[node[]], 1]
Out[71]= {node[$10]}
In[72]:= root.getChild[1].getChild[1].setValue[10]
Out[72]= 10
In[73]:= root.getChild[1].getChild[1].getValue[]
Out[73]= 10
Pour un exemple non trivial d'utilisation de cette structure de données arborescente mutable, voir ce de mon poste. Il confronte également cette méthode à celle qui réutilise plus fortement les structures de données et fonctions natives de Mathematica, et illustre bien les points abordés au début de ce billet.