Salut
Pour une raison quelconque, notre calculatrice est associĂ©e Ă quelque chose que chaque dĂ©butant devrait Ă©crire. Peut-ĂȘtre parce que les ordinateurs ont Ă©tĂ© créés Ă cet effet pour compter. Mais nous Ă©crirons une calculatrice difficile, pas sympa bien sĂ»r, mais pour pouvoir faire des opĂ©rations algĂ©briques de base, comme la diffĂ©renciation, la simplification, et des fonctionnalitĂ©s comme la compilation pour accĂ©lĂ©rer les calculs.
Moins d'eau! De quoi parle l'article?Ici, il sera superficiel de construire une expression, d'analyser à partir d'une chaßne, de substitution de variables, de dérivé analytique, de résoudre numériquement une équation et une certaine intégrale, de rendre au format LaTeX, de nombres complexes, de compiler des fonctions, de simplifier, d'étendre les crochets et de bla bla bla. Probablement pas dans un article.
Pour ceux qui ont un besoin urgent de cloner quelque chose, un lien vers le référentiel . Nous prenons les cookies restants de la nouvelle année, et avons roulé!
Ă qui s'adresse cet article?Je pense que l'article peut ĂȘtre utile Ă un dĂ©butant, mais peut-ĂȘtre que ceux qui sont un peu plus expĂ©rimentĂ©s trouveront aussi quelque chose d'intĂ©ressant. Cependant, j'espĂšre Ă©crire un article afin qu'il puisse ĂȘtre lu sans ĂȘtre du tout programmeur C #.
Assemblage d'expression
Qu'est-ce qu'une expression?
Quand j'étais petit ...Ensuite, bien sûr, je voulais écrire une calculatrice. Que devrait-il pouvoir faire? Quatre opérations de base, et en principe bien plus. Donc, ma tùche était de calculer la valeur d'une expression de chaßne, par exemple, «1 + (3/4 - (5 + 3 * 1))». J'ai pris mon dauphin préféré et j'ai écrit un analyseur, qui est d'abord récursivement placé entre crochets, puis a remplacé l'expression entre crochets par une valeur et a supprimé les crochets. Fondamentalement, une façon tout à fait fonctionnelle pour moi à cette époque.
Bien sĂ»r, ce n'est pas une ligne. Il est assez Ă©vident qu'une formule mathĂ©matique est soit un arbre, soit une pile, et ici nous nous arrĂȘtons au premier. Autrement dit, chaque nĆud, chaque nĆud de cet arbre, est une sorte d'opĂ©ration, variable ou constante.

Une opĂ©ration est soit une fonction, soit un opĂ©rateur, en principe, Ă peu prĂšs la mĂȘme chose. Ses enfants sont les arguments d'une fonction (opĂ©rateur).
Hiérarchie des classes dans votre code
Bien sĂ»r, l'implĂ©mentation peut ĂȘtre quelconque. Cependant, l'idĂ©e est que si votre arbre se compose uniquement de nĆuds et de feuilles, ils sont diffĂ©rents. Par consĂ©quent, j'appelle ces «choses» - des entitĂ©s. Par consĂ©quent, la classe supĂ©rieure sera l'entitĂ© de classe abstraite.
Abstrait?Comme tout le monde le sait grĂące Ă l'apprentissage des langues de base, une classe abstraite est bonne car elle gĂ©nĂ©ralise certaines classes d'une part, et d'autre part vous permet de sĂ©parer la logique et le comportement de certains objets. Un objet d'une classe abstraite ne peut pas ĂȘtre créé, mais son hĂ©ritier le peut.
Et il y aura également quatre classes successives: NumberEntity, VariableEntity, OperatorEntity, FunctionEntity.
Comment construire une expression?
Tout d'abord, nous allons construire une expression dans le code, c'est-Ă -dire
var x = new VariableEntity("x"); var expr = x * x + 3 * x + 12;
Si vous déclarez une classe VariableEntity vide, un tel code vous générera une erreur, disent-ils, ne sait pas comment multiplier et additionner.
Remplacer les opérateurs
Une fonctionnalité trÚs importante et utile de la plupart des langues, vous permettant de personnaliser l'exécution des opérations arithmétiques. Il est implémenté syntaxiquement différemment selon la langue. Par exemple, une implémentation en C #
public static YourClass operator +(YourClass a, YourClass b) { return new YourClass(a.ToString() + b.ToString()); }
En savoir plus sur la substitution des instructions en C #Le navet est implémenté
ici .
Casting (non) explicite
Dans les langages compilés tels que C #, une telle chose est généralement présente et vous permet de transtyper le type si nécessaire sans appeler en plus myvar.ToAnotherType (). Ainsi, par exemple, il serait pratique d'écrire
NumberEntity myvar = 3;
Au lieu de l'habituel
NumberEntity myvar = new NumberEntity(3);
En savoir plus sur la coulée de caractÚres en C #Le navet est mis en place sur
cette ligne.
Accrocher
La classe Entity a un champ Enfants - ce n'est qu'une liste d'entités, qui sont les arguments de cette entité.
RéflexionsEn fait, seules deux classes d'objets peuvent avoir des enfants: OperatorEntity et FunctionEntity. Autrement dit, vous pouvez en principe créer une sorte de NodeEntity et en hériter ces deux classes, créer une LeafEntity et en hériter VariableEntity et NumberEntity.

Lorsque nous appelons une fonction ou un opérateur, nous devons créer une nouvelle entité et y mettre les enfants à partir desquels la fonction ou l'opérateur est appelé. Par exemple, le montant en théorie devrait ressembler à ceci:
public static Entity operator +(Entity a, Entity b){ var res = new OperatorEntity("+"); res.Children.Add(a); res.Children.Add(b); return res; }
Autrement dit, si nous avons l'entité x et l'entité 3, alors x + 3 renverra l'essence de l'opérateur de somme avec deux enfants: 3 et x. Ainsi, nous pouvons construire des arbres d'expression.
Un appel de fonction est plus simple et pas aussi beau qu'avec un opérateur:
public Entity Sin(Entity a) { var res = new FunctionEntity("sin"); res.Children.Add(a); return res; }
La pendaison de navets est implémentée
ici .
Ok, nous avons créé un arbre d'expression.
Substitution variable
Ici, tout est extrĂȘmement simple. Nous avons Entity - nous vĂ©rifions s'il s'agit d'une variable elle-mĂȘme, si c'est le cas, nous renvoyons la valeur, sinon nous parcourons les enfants.
Cet énorme fichier de 48 lignes implémente une fonction aussi complexe.
Calcul de la valeur
En fait, pour ce que tout cela est. Ici, nous sommes censés ajouter une sorte de méthode à Entity
public Entity Eval() { if (IsLeaf) { return this; } else return MathFunctions.InvokeEval(Name, Children); }
La feuille est inchangée, mais pour tout le reste, nous avons un calcul personnalisé. Encore une fois, je vais donner un exemple:
public static Entity Eval(List<Entity> args) { MathFunctions.AssertArgs(args.Count, 1); var r = args[0].Eval(); if (r is NumberEntity) return new NumberEntity(Number.Sin((r as NumberEntity).Value)); else return r.Sin(); }
Si l'argument est un nombre, alors nous produirons une fonction numérique, sinon nous le renverrons tel qu'il était.
Numéro?
C'est l'unitĂ©, le nombre le plus simple. Des opĂ©rations arithmĂ©tiques peuvent y ĂȘtre effectuĂ©es. Par dĂ©faut, c'est complexe. Il a Ă©galement des opĂ©rations telles que Sin, Cos et d'autres dĂ©finies.
Si vous ĂȘtes intĂ©ressĂ©, le numĂ©ro est dĂ©crit
ici .
Dérivé
Tout le monde peut calculer la dérivée numériquement, et une telle fonction s'écrit vraiment sur une seule ligne:
public double Derivative(Func<double, double> f, double x) => (f(x + 1.0e-5) - f(x)) * 1.0e+5;
Mais bien sĂ»r, nous voulons un dĂ©rivĂ© analytique. Puisque nous avons dĂ©jĂ un arbre d'expression, nous pouvons remplacer rĂ©cursivement chaque nĆud conformĂ©ment Ă la rĂšgle de diffĂ©renciation. Cela devrait fonctionner comme ceci:

Voici, par exemple, comment le montant est implémenté dans mon code:
public static Entity Derive(List<Entity> args, VariableEntity variable) { MathFunctions.AssertArgs(args.Count, 2); var a = args[0]; var b = args[1]; return a.Derive(variable) + b.Derive(variable); }
Mais le travail
public static Entity Derive(List<Entity> args, VariableEntity variable) { MathFunctions.AssertArgs(args.Count, 2); var a = args[0]; var b = args[1]; return a.Derive(variable) * b + b.Derive(variable) * a; }
Et voici une solution de contournement en soi:
public Entity Derive(VariableEntity x) { if (IsLeaf) { if (this is VariableEntity && this.Name == x.Name) return new NumberEntity(1); else return new NumberEntity(0); } else return MathFunctions.InvokeDerive(Name, Children, x); }
Il s'agit de la mĂ©thode Entity. Et comme nous le voyons, la feuille n'a que deux Ă©tats - soit c'est une variable par laquelle nous diffĂ©rencions, puis sa dĂ©rivĂ©e est 1, ou c'est une constante (nombre ou VariableEntity), puis sa dĂ©rivĂ©e est 0, ou un nĆud, puis il y a une rĂ©fĂ©rence par nom (InvokeDerive fait rĂ©fĂ©rence au dictionnaire de fonctions, oĂč se trouve celle dĂ©sirĂ©e (par exemple, la somme ou le sinus)).
Notez que je ne laisse pas quelque chose comme dy / dx ici et dis tout de suite que la dérivée de la variable
non par laquelle nous différencions est 0. Mais ici,
cela se fait différemment.
Toute différenciation est décrite dans
un seul fichier , mais plus n'est pas nécessaire.
Simplification de l'expression. Patterns
La simplification de l'expression n'est généralement pas triviale en principe. Eh bien, par exemple, quelle expression est la plus simple:
x2ây2 ou
(xây)(x+y) ? Mais nous adhĂ©rons Ă certaines idĂ©es, et sur la base de celles-ci, nous voulons Ă©tablir ces rĂšgles qui simplifient prĂ©cisĂ©ment l'expression.
Il est possible d'Ă©crire Ă chaque Eval que si nous avons la somme et que les enfants sont des Ćuvres, alors nous trierons quatre options, et si quelque chose est Ă©gal quelque part, nous Ă©liminerons le facteur ... Mais bien sĂ»r, je ne veux pas faire ça. Par consĂ©quent, vous pouvez deviner le systĂšme de rĂšgles et de modĂšles. Alors que voulons-nous? Quelque chose comme cette syntaxe:
{ any1 / (any2 / any3) -> any1 * any3 / any2 }, { const1 * var1 + const2 * var1 -> (const1 + const2) * var1 }, { any1 + any1 * any2 -> any1 * (Num(1) + any2) },
Voici un exemple d'arbre dans lequel un sous-arbre a été trouvé (encerclé en vert) qui correspond au modÚle any1 + const1 * any1 (any1 trouvé est encerclé en orange).

Comme vous pouvez le voir, il est parfois important pour nous que la mĂȘme entitĂ© soit rĂ©pĂ©tĂ©e, par exemple, pour raccourcir l'expression x + a * x, nous avons besoin de x pour ĂȘtre lĂ et lĂ , car x + a * y n'est plus rĂ©duit. Par consĂ©quent, nous devons crĂ©er un algorithme qui non seulement vĂ©rifie que lâarbre correspond au modĂšle, mais
- VĂ©rifiez que le mĂȘme modĂšle d'entitĂ© correspond Ă la mĂȘme entitĂ©.
- Ecrire ce qui correspond Ă quoi, puis substituer.
Le point d'entrée ressemble à ceci:
internal Dictionary<int, Entity> EqFits(Entity tree) { var res = new Dictionary<int, Entity>(); if (!tree.PatternMakeMatch(this, res)) return null; else return res; }
Et dans tree.PaternMakeMatch, nous remplissons rĂ©cursivement le dictionnaire avec les clĂ©s et leurs valeurs. Voici un exemple de liste des entitĂ©s de modĂšle elles-mĂȘmes:
static readonly Pattern any1 = new Pattern(100, PatType.COMMON); static readonly Pattern any2 = new Pattern(101, PatType.COMMON); static readonly Pattern const1 = new Pattern(200, PatType.NUMBER); static readonly Pattern const2 = new Pattern(201, PatType.NUMBER); static readonly Pattern func1 = new Pattern(400, PatType.FUNCTION);
Lorsque nous Ă©crivons any1 * const1 - func1 et ainsi de suite, chaque nĆud aura un numĂ©ro - c'est la clĂ©. En d'autres termes, lors du remplissage du dictionnaire, ces nombres apparaĂźtront sous forme de clĂ©s: 100, 101, 200, 201, 400 ... Et lors de la construction d'un arbre, nous regarderons la valeur correspondant Ă la clĂ© et la remplacerons.
Mis en Ćuvre
ici .
Simplification. Tri des arbres
Dans l'
article que j'ai déjà abordé, l'auteur a décidé de le faire simplement, et trié pratiquement par hachage de l'arbre. Il a réussi à réduire a et -a, b + c + b pour tourner 2b + c. Mais nous voulons bien sûr aussi que (x + y) + x * y - 3 * x soit réduit, et en général des choses plus compliquées.
Les modĂšles ne fonctionnent pas?
En général, ce que nous faisions auparavant, les motifs sont une chose monstrueusement merveilleuse. Cela vous permettra de réduire la différence entre les carrés, et la somme des carrés du sinus et du cosinus, et d'autres choses complexes. Mais la paume élémentaire, ((((((x + 1) + 1) + 1) + 1), elle ne se réduira pas, car la rÚgle principale ici est la commutativité des termes. Par conséquent, la premiÚre étape consiste à isoler les «enfants linéaires».
"Enfants linéaires"
En fait, pour chaque nĆud de la somme ou de la diffĂ©rence (et, soit dit en passant, le produit / la division), nous voulons obtenir une liste de termes (facteurs).

C'est fondamentalement simple. Laissez la fonction LinearChildren (Entity node) renvoyer une liste, puis nous regardons l'enfant dans node.Children: si l'enfant n'est pas une somme, alors result.Add (enfant), sinon result.AddRange (LinearChildren (enfant)).
Pas la plus belle maniĂšre mise en Ćuvre
ici .
Regroupement d'enfants
Nous avons donc une liste d'enfants, mais que faire ensuite? Supposons que nous ayons sin (x) + x + y + sin (x) + 2 * x. De toute évidence, notre algorithme recevra cinq termes. Ensuite, nous voulons regrouper par similitude, par exemple, x ressemble à 2 * x de plus que sin (x).
Voici un bon regroupement:

Ătant donnĂ© que les modĂšles qu'il contient vont continuer Ă gĂ©rer la conversion de 2 * x + x en 3 * x.
Autrement dit, nous groupons d'abord par un
hachage , puis faisons MultiHang - conversion de la sommation n-aire en binaire.
Hachage de nĆud
D'une part
x et
x+1 devrait ĂȘtre placĂ© dans un groupe. D'un autre cĂŽtĂ©, si disponible
aâx mettre en un seul groupe avec
yâ(x+1) inutile.
RĂ©flexionsSi vous y pensez, alors aâx+yâ(x+1)=(a+y)âx+y . Bien qu'il me semble, ce n'est pratiquement pas plus facile, et certainement pas nĂ©cessaire. Quoi qu'il en soit, la simplification n'est jamais une chose Ă©vidente, et ce n'est certainement pas la premiĂšre chose Ă Ă©crire lors de l'Ă©criture d'une «calculatrice».
Par conséquent, nous implémentons le tri à plusieurs niveaux. Tout d'abord, nous prétendons que
x+1 - la mĂȘme chose. TriĂ©, calmĂ©. Ensuite, nous prĂ©tendons que
x+1 ne peut ĂȘtre placĂ© qu'avec d'autres
x+1 . Et maintenant notre
aâ(x+1) et
yâ(x+1) enfin fait Ă©quipe. Mis en Ćuvre tout simplement:
internal string Hash(SortLevel level) { if (this is FunctionEntity) return this.Name + "_" + string.Join("_", from child in Children select child.Hash(level)); else if (this is NumberEntity) return level == SortLevel.HIGH_LEVEL ? "" : this.Name + " "; else if (this is VariableEntity) return "v_" + Name; else return (level == SortLevel.LOW_LEVEL ? this.Name + "_" : "") + string.Join("_", from child in Children where child.Hash(level) != "" select child.Hash(level)); }
Comme vous pouvez le voir, la fonction affecte le tri de quelque maniÚre que ce soit (bien sûr, car
f(x) avec
x généralement pas connecté dans le cas général). Comme une variable,
x avec
y Eh bien, ça ne va pas se mĂ©langer. Mais les constantes et les opĂ©rateurs ne sont pas pris en compte Ă tous les niveaux. Dans cet ordre, le processus de simplification lui-mĂȘme
public Entity Simplify(int level) {
RĂ©flexionsEst-ce la meilleure mise en Ćuvre? InsĂ©rez des messages privĂ©s, il y aura peut-ĂȘtre de meilleures idĂ©es. J'ai longtemps pensĂ© comment le faire le plus magnifiquement possible, mĂȘme si Ă mon avis, c'est loin d'ĂȘtre «beau».
Je trie l'arbre
ici .
"Compilation" des fonctions
Entre guillemets - car ce n'est pas dans le code IL lui-mĂȘme, mais seulement dans un ensemble d'instructions trĂšs rapide. Mais c'est trĂšs simple.
ProblĂšme de remplacement
Pour calculer la valeur d'une fonction, il suffit d'appeler la substitution de variable et eval, par exemple
var x = MathS.Var("x"); var expr = x * x + 3; var result = expr.Substitute(x, 5).Eval();
Mais cela fonctionne lentement, environ 1,5 microsecondes par sinus.
Instructions
Pour accélérer le calcul, nous effectuons un calcul de fonction sur la pile, à savoir:
1) Nous arrivons avec la classe FastExpression, qui aura une liste d'instructions
2) Lors de la compilation des instructions sont empilées dans l'ordre inverse, c'est-à -dire, s'il y a une fonction x * x + sin (x) + 3, alors les instructions seront quelque chose comme ceci:
PUSHVAR 0 // 0 - x CALL 6 // 6 - PUSHCONST 3 CALL 0 // 0 - PUSHVAR 0 PUSHVAR 0 CALL 2 CALL 0
Ensuite, une fois appelé, nous exécutons ces instructions et retournons Number.
Un exemple d'exécution d'une instruction sum:
internal static void Sumf(Stack<Number> stack) { Number n1 = stack.Pop(); Number n2 = stack.Pop(); stack.Push(n1 + n2); }
L'appel sinus a été réduit de 1500 à 60 n (le systÚme Complex.Sin fonctionne pendant 30 n).
Le navet est implémenté
ici .
Fuh, tout semble ĂȘtre pour le moment. Bien qu'il y ait encore quelque chose Ă dire, mais il me semble que le volume d'un article est suffisant. Quelqu'un est-il intĂ©ressĂ© par la suite? Ă savoir: analyse Ă partir d'une chaĂźne, mise en forme en latex, une certaine intĂ©grale et autres goodies.
Lien vers le référentiel avec tout le code, ainsi que des tests et des échantillons.
RéflexionsEn fait, je continue de travailler sur ce projet. Il est distribué sous MIT (c'est-à -dire, faites ce que vous voulez avec lui), et il ne deviendra jamais fermé ou commercial. De plus, s'il existe des idées d'amélioration et de contribution, les demandes de tirage sont les bienvenues.
Merci de votre attention!