Nous écrivons une «calculatrice» en C #. Partie I. Calcul de la valeur, dérivé, simplification et autres oies

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Ă©flexions
En 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

  1. VĂ©rifiez que le mĂȘme modĂšle d'entitĂ© correspond Ă  la mĂȘme entitĂ©.
  2. 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Ă©flexions
Si 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) { //      :   ,   ,     . . var stage1 = this.InnerSimplify(); Entity res = stage1; for (int i = 0; i < level; i++) { //     .    -  x  x+1 (  ),  -  x-1  x+1 (,   ),  -  x+1  x+1 ( ). switch (i) { case 0: res = res.Sort(SortLevel.HIGH_LEVEL); break; case 2: res = res.Sort(SortLevel.MIDDLE_LEVEL); break; case 4: res = res.Sort(SortLevel.LOW_LEVEL); break; } //    . res = TreeAnalyzer.Replace(Patterns.CommonRules, res).InnerSimplify(); } return res; } 

RĂ©flexions
Est-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Ă©flexions
En 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!

Source: https://habr.com/ru/post/fr482228/


All Articles