Computation avec JavaScript

De EduTech Wiki
Aller à la navigation Aller à la recherche

Cet article est en construction: un auteur est en train de le modifier.

En principe, le ou les auteurs en question devraient bientôt présenter une meilleure version.



Introduction

Cette page complémente le Tutoriel JavaScript de base avec l'analyse d'exemples d'algorithmes en JavaScript. Les exemples sont censés montrer des petites applications ou des simples jeux, mais sans l'interface utilisateur qui est traitée plutôt dans le Tutoriel JavaScript côté client et, surtout, dans la page Interactivité avec JavaScript qui utilise une approche avec des exemples expliqués de manière détaillée.

L'objectif de cet article est de montrer, avec des exemples pratiques, comment on peut combiner les éléments fondamentaux de la programmation pour construire une solution automatisée qui permet d'atteindre un objectif déterminé, que se soit un petit jeux ou du traitement de l'information.

Avant de voir les exemples, nous allons définir très superficiellement le concept de computation et le mettre en relation avec la programmation. Ensuite, nous verrons de manière technique comment se passe la computation/évaluation du code en JavaScript.

Prérequis

Pour lire cet article, des notions sur les éléments fondamentaux de la programmation, illustrés dans l'introduction à la programmation peuvent faciliter la compréhension à la fois des termes techniques et des concepts auxquels ils se réfèrent. Une exposition au moins à la syntaxe de JavaScript, disponible dans la deuxième section du Tutoriel JavaScript de base, est également conseillée, même si des références croisées à cette page sont disponibles surtout dans les premiers exemples.

Exemples traités dans l'article

Occasionnellement, nous allons utiliser un prompt() pour simuler des inputs différents, même si cet élément est disponible exclusivement dans le navigateur.

Les exemples sont censés fonctionner dans tous les environnements où JavaScript peut être exécuté et par conséquent ils peuvent être testé directement dans la console du navigateur comme il a été suggéré dans le Tutoriel JavaScript de base. La seule exception concerne l'utilisation dans certains exemples de l'élément prompt() qui appartient à l'environnement JavaScript côté client, mais qui nous servira pour simuler facilement des inputs différents que vous aurez à fournir à travers la fenêtre.

Tous les exemples illustrés dans cette page sont disponibles dans un repository de GitHub. Ils suivent une numérotation qui sera utilisée pour les référencer également dans le texte, par exemple: 00-01.

Lorsque le code des exemples est expliqué dans l'article, pour des raisons d'espace, souvent seulement les parties indispensables à la compérhension du concept expliqué seront affichés. En général, les numéros des lignes intéressées sera fourni pour pouvoir repérer le code dans les fichiers, par exemple 00-01 Lignes 10-20.

Définition de computation

Schéma de la Théorie de la computation, inspiré par Sipser (2012)

Pour définir la computation, nous allons introduire brièvement et de manière très superficielle la théorie de la computation (Sipser, 2012). Cette théorie s'intéresse à la question fondamentale de savoir quels sont les possibilités et, par conséquent, les limites (ou non possibilités) des calculateurs (Sipser, 2012). Par calculateurs il faut entendre tout système, physique ou théorique, qui permet de faire des calcules. En effet, cette théorie peut être vue sur un continuum entre deux pôles :

  • le pôle théorique qui est plus étroitement lié à la logique et aux mathématiques ;
  • le pôle appliqué qui est plus proche des objectifs de cette page et s'occupe d'implémenter physiquement les aspects théoriques dans des machines ou dans des programmes.

La théorie de la computation émerge de l'interaction de trois sous-théories (Sipser, 2012) :

  1. La théorie de la complexité
    Cette branche analyse les raisons pour lesquelles certains problèmes sont faciles à résoudre et d'autres sont plus complexes.
  2. La théorie de la computation
    Cette branche s'intéresse aux problèmes de manière dichotomique, en essayant de déterminer quels problèmes ont une solution possible et ceux qui, au contraire, ne peuvent pas être résolus.
  3. La théorie des automates
    Cette branche concerne tous les éléments qui peuvent faire eux-mêmes ou contribuer à exécuter des calculs. Dans le cadre de nos objectifs pour cet article, on retrouve dans cette branche les ordinateurs ou dispositifs numériques en général, les compilateurs/interprètes, ainsi que les langages de programmation.

Nous introduisons ces concepts très abstraits pour illustrer que les dispositifs numériques qu'on utilise aujourd'hui sont directement lié à cette théorie, car on fait remonter typiquement la naissance de l'informatique (ou Computer Science) aux travaux de Alonzo Church et Alan Turing à propos du problème de la décision. En essayant de simplifier au maximum ces concepts, il s'agit de la possibilité de créer un système mécanique qui soit capable de déterminer si la solution trouvé à un problème à travers des algorithmes est correcte. La machine de Turing, un modèle théorique de calculateur, est le résultat de cette tentative et elle est à la base des dispositifs numériques qu'on utilise aujourd'hui. La plupart de ces dispositifs, en effet, est basé sur ce qu'on appelle l'architecture de Von Neumann, d'après le mathématicien John von Neumann, qui s'est inspiré aux travaux de Turing.

Définition informelle pour les objectifs de cet article

La théorie de la computation dépasse largement les objectifs de cet article, mais on peut néanmoins s'en servir pour formuler une définition informelle de la computation plus adaptée au contexte de cet article. On peut considérer le développement comme un processus qui consiste à :

  1. Réduire la complexité d'un problème ou d'un besoin, par exemple une application interactive qui favorise l'apprentissage d'une langue étrangère, à une suite d'instructions automatisées.
  2. Traduire ces instructions en code JavaScript, afin que l'interprète puisse le computer de manière correspondante aux attentes.

Pour atteindre cet objectif, il faut déployer les principes de la pensée computationnelle - décomposition, reconnaissance de pattern, abstraction et construction d'algorithmes - car l'interprète n'accepte que des instructions très simples. S'il existait une fonction JavaScript déjà disponible, du type

Learn('Italiano') = Io parlo perfettamente l'italiano

on l'utiliserait ! Mais cela n'est pas le cas et par conséquent il faut faire recours aux blocs constitutifs de la programmation (variables, boucles, structure de contrôle, ...) qui sont construit de manière flexibles afin de pouvoir répondre à la plupart des exigences en termes computationnels.

La traduction en code JavaScript nécessite de deux connaissances qui s'influencent mutuellement dans un cycle itérative :

  1. Savoir réduire toutes les fonctionnalités qui composent l'application dans du pseudo code
    Le pseudo code est une forme hybride entre le langage naturel des humains et le code d'une machine qui permet de décrire le fonctionnement d'un programme de manière plutôt général. Exemples de phrases en pseudo code peuvent ressembler à :
    • Pose des questions jusqu'à que l'apprenant n'a pas eu au moins 10 réponses correctes
    • Affiche une réponse correcte et 4 distracteurs
    • Si le score est majeur de 100, passe au niveau supérieur
  2. Savoir passer du pseudo code au code JavaScript
    Le pseudo code, en tant que méta-algorithme, ne peut pas être exécuté par une machine. Il est donc nécessaire de maîtriser les règles syntaxiques et exploiter les éléments propre à JavaScript pour obtenir les computations/instructions du pseudo code.

Cet article se pose l'objectif très ambitieux de formuler des exemples qui permettent d'améliorer les connaissances sur les deux fronts, même si le focus est plutôt axé sur le deuxième aspect, plus technique. Pour cette raison, nous illustrerons dans la section suivante le fonctionnement de l'interprète JavaScript.

Principe technique de la computation avec JavaScript

Dans cet partie de l'article, nous aborderons le fonctionnement de l'évaluation du code JavaScript par l'interprète. Avec des exemples très simples, nous illustrerons ce qu'on appelle le parsing, c'est-à-dire l'analyse syntaxique appliquée au code source. En effet, les langages de programmation appliquent eux-mêmes, par nécessité, les principes de décomposition et reconnaissance de pattern. Le code source est décomposée en une série de petites instructions, et ces instructions doivent suivre des règles syntaxique : l'utilisation de mots que l'interprète reconnait et dans un ordre qu'il arrive à comprendre. Pour une explication plus approfondie de ce mécanisme, voir Simpson (2015).

Fonctionnement d'une assignation de valeur à une variable

Commençons par analyser une simple assignation d'une valeur littérale à une référence symbolique simple : une variable.

var language = "JavaScript";

Cette simple instruction se traduit en langage courant de la manière suivante :

  1. Crée une boîté avec l'étiquette language
  2. Met à son intérieur le mot JavaScript.
Lecture du code source du point de vue de l'interprète qui décompose le code source à la recherche de pattern qu'il reconnaît.

Pour l'interprète cette simple instruction se compose en réalité de 5 passages :

  1. Reconnaît le mot var. Une nom de la variable est attendu.
  2. Reconnaît le nom de variable language comme un nom qui respecte les règles syntaxique des noms des variables. Un symbole d'affection à ce point est optionnel.
  3. Reconnaît le symbole d'affection =. Une expression qui détermine une valeur est attendue.
  4. Reconnaît l'expression "JavaScript" comme une suite de caractères composée par les caractères J, a, v, a, S, c, r, i, p, t.
  5. Reconnaît le ; qui détermine la fin d'une instruction.

À ce point, l'interprète sait qu'il peut créer dans la logique de l'application une référence symbolique identifiable avec le nom language. Chaque fois qu'il reconnaît à nouveau ce nom, la suite de caractère JavaScript sera utilisé à sa place.

Évaluation d'une expression après le symbole d'affectation

L'affectation de la variable ne se fait pas avant la concaténation.
"Java" + "Script" est évalué avant l'affectation de la variable language.

Si on modifie légèrement le code, tout simplement en divisant le mot JavaScript en deux parties - "Java" et "Script" - et en utilisant l'opérateur + pour les concaténer, la situation change :

var language = "Java" + "Script";

À ce moment, l'évaluation du code reflète les étapes vue plus haut, mais jusqu'à l'étape 3. À partir de l'étape 4, les choses se modifient, parce que si on avait gardé le même principe de l'exemple précédent, on aurait comme résultat l'affectation de la variable language à la suite de caractères Java et dans un deuxième temps on ajoute Script. Mais dans ce cas, dans la suite du code, lorsqu'on utilise language, on ferait référence seulement à Java et non pas à JavaScript.

L'évaluation du code ne suit pas, donc, l'ordre précis des mots, mais évalue des patterns qui peuvent avoir des priorités différentes. Dans ce cas, tout ce qui se passe sur la droite du symbole d'affectation = a la précédence sur l'ensemble de la ligne du code. Une fois que l'expression "Java" + "Script" a été évaluée, l'interprète retourne en arrière et affecte "JavaScript" à la variable language. Grâce à ce mécanisme, notre language sera donc JavaScript et non pas Java : un grand avantage !

Réutilisation d'une variable dans l'affectation de la même variable

L'ordre d'évaluation permet d'utiliser une variable dans l'affectation d'une nouvelle valeur à la variable elle-même

L'importance de l'ordre du parsing est plus évident si on modifie l'exemple en décomposant "JavaScript" en deux variables, l'une avec la valeur "Java" et l'autres avec la valeur "Script" :

1 var language = "Java";
2 var correction = "Script";
3 language = language + correction;

Vous pouvez noter que pour corriger notre erreur dans l'affectation du nom du langage, à la ligne trois nous faisons référence à la fausse valeur initial, à la quelle on ajoute la correction. Grâce au mécanisme de parsing, cette expression est évaluée en amont, et donc on peut très bien associer cette nouvelle valeur computée à la variable language elle-même.

Exemples étape par étape

Dans cette section, nous allons analyser de manière détaillée quelques exemples de code, où des principes de computation sont adoptée pour faciliter ou améliorer les instructions passées à l'interprète JavaScript. Les exemples sont souvent retravaillé au fil des passages pour montrer que la construction d'un algorithme n'est pas une démarche qui se fait au premier coup ; au contraire, il faut souvent revoir le code, parfois même le refaire.

Voici un aperçu des exemples et des principes que nous allons voir :

  • Manipulation des valeurs d'une variable
    À travers le score à un test d'apprentissage imaginaire, nous illustrerons l'utilisation des variables pour stocker, récupérer et modifier une valeur
  • À faire...

Comme il a été le cas pour le Tutoriel JavaScript de base, nous conseillons de tester et jouer avec le code directement dans la console de votre navigateur. Dans la plupart des navigateurs, il suffit de cliquer sur F12 pour l'ouvrir.

Manipulation des valeurs d'une variable

Déclaration d'une variable

Dans cet exemple, nous allons imaginer des simples algorithmes qui permettent de modifier le score, par exemple à un test d'apprentissage. Pour ce faire, nous allons d'abord créer une référence symbolique au score à travers la déclaration d'une variable comme dans le fichier 01-01 :

1 var score = 0;

La décision de faire démarrer le score à 0 est tout à fait conventionnelle, on pourrait imaginer n'importe quelle valeur de départ. Le choix du nom de la variable est également arbitraire, mais ceci nous permet d'avoir une idée précise de l'utilité de la variable : elle sert justement à stocker le score actuel de apprenant.

Modification de la valeur à travers une fonction

Nous allons maintenant modifier la valeur initial du score avec une fonction. Idéalement, cette fonction devrait être appelée chaque fois que l'apprenant donne une réponse correcte. Voici le code de l'exemple 01-02 :

 1 var score = 0;
 2 
 3 //Declare the function
 4 function incrementScore() {
 5   //increment the score
 6   score = score + 1;
 7 }
 8 
 9 //Use the function three times
10 incrementScore(); //--> score = 1
11 incrementScore(); //--> score = 2
12 incrementScore(); //--> score = 3
Illustration du lexical scope, un concept assez complexe à retenir, mais important pour éviter des mauvaises surprises dans votre code.

Cet exemple très simple nous permet cependant de voir qu'on peut à la fois lire et modifier la valeur de la variable score à l'intérieur de la fonction incrementScore(). Ce principe s'appelle en termes informatiques Lexical Scope. Le scope, ou portée en français, détermine le lien entre une variable et sa valeur dans le bout de code dans laquelle elle est utilisée. Une fonction en JavaScript est considéré un bout de code à part, en ayant donc son propre scope. Cependant, grâce au mécanisme du lexical scope, la fonction incrementScore() a accès à la variable score pour deux raisons :

  1. Parce que dans notre cas var score a été déclarée dans le global scope (c'est-à-dire le niveau supérieur du code) et donc elle sera accessible dans toutes les fonctions, n'importe à quel niveau d'emboîtement elles ont été déclarée. On peut en effet déclarer une fonction à l'intérieur d'une autre fonction, etc.
  2. Même si cela n'avait pas été le cas, la variable score et la fonction incrementScore() ont été définies au même niveau du scope.

Comprendre le scope est l'une des challenges les plus ambitieuses dans la programmation en JavaScript, mais au moins une compréhension superficielle permet d'éviter des erreurs, surtout au niveau des attentes d'avoir accès à la valeur d'une variable quand en réalité on l'a pas.

Donc, comme il est illustré par la figure sur la droite, notre fonction incrementScore() peut :

  1. D'abord lire la valeur actuelle de score et l'incrémenter d'une unité
  2. Associer la nouvelle valeur à la variable elle-même

Utiliser un argument pour rendre la fonction plus flexible

On peut très bien imaginer que notre test d'apprentissage propose des questions qui diffèrent dans la difficulté, et sont par conséquent associées à des incrémentations différentes. On peut donc modifier notre fonction incrementScore() en utilisant un argument qui détermine le nombre de points à ajouter. Voici le code de l'exemple 01-03 :

 1 var score = 0;
 2 
 3 //Declare the function and accept an argument as number of points to add
 4 //In this way we have a more flexible function
 5 function incrementScoreBy(points) {
 6   //increment the score
 7   score = score + points;
 8 }
 9 
10 //Use the function three times
11 incrementScoreBy(5); //--> score = 5
12 incrementScoreBy(10); //--> score = 15
13 incrementScoreBy(50); //--> score = 65

Par rapport à la version précédente, nous avons légèrement modifié le nom de la fonction en ajoutant le suffixe By qui suggère qu'on s'attend à un argument. Cet argument est ensuite tout simplement utilisé pour incrémenter le score du chiffre respectif.

Modifier la valeur à travers une autre fonction

Le même principe s'applique si on veut utiliser une pénalité suite à une mauvaise réponse. On pourrait imaginer d'utiliser la même fonction en passant des nombres négatifs, mais c'est peut être plus pratique d'avoir une deuxième fonction qu'on va appeler decrementScoreBy() et qui est ajoutée dans le code de l'exemple 01-04 :

 1 var score = 0;
 2 
 3 //Function to add points
 4 function incrementScoreBy(points) {
 5   //increment the score
 6   score = score + points;
 7 }
 8 
 9 //Function to substract points
10 function decrementScoreBy(points) {
11   //decrement the score
12   score = score - points;
13 }
14 
15 //Use both functions
16 incrementScoreBy(20); //--> score = 20
17 decrementScoreBy(5); //--> score = 15
18 incrementScoreBy(15); //--> score = 30
19 decrementScoreBy(20); //--> score = 10

Déterminer la valeur de manière conditionnelle

Le mécanisme de pénalité introduit dans l'exemple précédent peut faire ainsi que le score de l'apprenant soit négatif, ce qui est peut être exagéré et pourrait contribuer à frustrer la personne et la faire désister. Donc on peut définir la règle suivante :

Si le score est inférieur à 0, remet-le à 0

On implémente cette logique à l'intérieur de la fonction decrementScoreBy() dans l'exemple 01-05 :

 9 //Function to substract points
10 function decrementScoreBy(points) {
11   //decrement the score
12   score = score - points;
13   //At this point, check if it is below zero
14   if (score < 0) {
15     //Set it back to zero
16     score = 0;
17   }
18 }
19 
20 //Use both functions
21 incrementScoreBy(20); //--> score = 20
22 decrementScoreBy(50); //--> score = 0
23 incrementScoreBy(15); //--> score = 15
24 decrementScoreBy(5); //--> score = 10

Avec l'ajout de ce code, le score ne pourra jamais être négatif après une pénalité. C'est important de bien noter cet aspect : le score est contrôlé seulement s'il est modifié à travers la fonction decrementScoreBy(). Si jamais d'autres fonctions devaient modifier la valeur en négatif, ce mécanisme de contrôle ne serait pas activé. Il faudrait donc à la rigueur l'implémenter aussi dans d'autres fonctions qui risquent de rendre le score négatif, ou trouver un mécanisme plus élaboré - et qui dépasse le cadre de cet article - pour contrôler le score à chaque modification, n'importe à quel endroit du code elle a été faite.

Réduire la probabilité de faire des erreurs dans le code

Notre algorithme est déjà plus ou moins aboutis en ce qui concerne sa fonction de base : augmenter ou diminuer le score en fonction des réponses de l'apprenant. La fonction decrementScoreBy(), cependant, peut générer une certaine confusion au sujet à la valeur à passer : faut-il passer un score positive ou négative pour le décrémenter ?

Notre code est relativement court à présent, et donc en cas de doute on peut toujours contrôler et s'assurer depuis le code qu'il faut un nombre positif qui sera ensuite soustrait. Mais il est très probable que nous allons utiliser cette fonction à plusieurs reprises au fur et à mesure que notre application se complexifie. Utiliser par distraction la fonction avec un nombre négatif aurait une grave conséquence au niveau du fonctionnement du test : decreaseScoreBy(-50) ferait en réalité gagner 50 points à l'apprenant !

Parmi toutes les alternative possibles, on peut identifier deux solutions à ce problème :

  1. Générer un message d'erreur et arrêter le script ;
  2. Ajouter une transformation de l'argument ainsi qu'il n'y a pas de différence si la valeur passée est 50 ou -50

Dans l'exemple 01-06 nous avons opté pour la deuxième solution qui permet d'utiliser cette fonction en toute tranquillité (du moins qu'on passe des valeurs de type number et pas d'autres !) :

 9 //Function to substract points
10 function decrementScoreBy(points) {
11   //Force number to be positive using Math.abs()
12   points = Math.abs(points);
13   //decrement the score
14   score = score - points;
15   //At this point, check if it is below zero
16   if (score < 0) {
17     //Set it back to zero
18     score = 0;
19   }
20 }
21 
22 //Use both functions
23 incrementScoreBy(20); //--> score = 20
24 decrementScoreBy(-50); //--> score = 0
25 incrementScoreBy(15); //--> score = 15
26 decrementScoreBy(5); //--> score = 10

Grâce à la méthode Math.abs() qui transforme n'importe quel nombre dans son absolu - et donc positif - on peut être sûr que la valeur passé à la fonction decrementScoreBy() sera de toute manière soustraite au score totale, que ce soit une valeur positive ou négative.

On pourrait se demander à ce point s'il vaut la peine d'implémenter le même contrôle également dans la fonction incrementScoreBy(), car potentiellement on pourrait avoir le mécanisme inverse :

incrementScoreBy(-50); //--> équivaut à soustraire 50 au score

Nous vous laissons considérer cet aspect et éventuellement l'aborder avec les exercices de consolidation proposés dans le point suivant.

Exercices de consolidation

Nous proposons de suite deux exerices de consolidation des éléments traités dans ce premier exemple, un plus simple, et l'autre plus difficile.

Simple
  • Imaginez que le test est réussi si l'apprenant totalise au moins 100 points ;
  • Implémentez un contrôle dans la fonction incrementScoreBy() qui affiche un message de congratulations si ce score est atteint ou dépassé
Plus difficile
  • Imaginez un système de niveaux qui changent en fonction du score ;
  • Chaque niveau est représenté par 10 points (de 0 à 10, niveau 1 ; de 11 à 20, niveau 2 ; etc.)
  • Ajoutez une variable qui tient compte du niveau
  • Ajoutez des mécanismes qui modifient automatiquement le niveau en fonction de la variation du score (en positif et en négatif)

Conclusion

Bibliographie

  • Sipser, M. (2012). Introduction to the Theory of Computation (3rd ed.). Cengage Learning. (Oeuvre très spécifique)
  • Simpson, K. (2015). You Don’t Know JS: Up & Going. Sebastopol, CA: O’Reilly Media.

Ressources