Programmation Javascript (NFA041)

Bases de programmation illustrées en javascript,

fonctions d'ordre supérieur, modèle d’exécution

Olivier Pons
(olivier.pons@lecnam.net)

2022

Avant de commencer

Quelques méthodes utiles sur les chaines et les tableaux

sur les chaines

let s="JAVASCRIPT est un langage"
> s.toUpperCase()
'JAVASCRIPT EST UN LANGAGE'
> s.toLowerCase()
'javascript est un langage'
> 
> s.replace("JAVASCRIPT","C")
'C est un langage'
> 
> s.substring(5,8)
'CRI'
> 
> s.split(" ")                            // coupe par rapport a " ", sépare les mots
[ 'JAVASCRIPT', 'est', 'un', 'langage' ]  //le résultat est un tableau de mot
> s.split("a")                             // coupe par rapport a "a"
[ 'JAVASCRIPT est un l', 'ng', 'ge' ]      
> s.split("")                         // coupe par rapport a "" 
[                                     // donne un tableau de lettre
  'J', 'A', 'V', 'A', 'S', 'C',
  'R', 'I', 'P', 'T', ' ', 'e',
  's', 't', ' ', 'u', 'n', ' ',
  'l', 'a', 'n', 'g', 'a', 'g',
  'e'
]
> 

sur les tableaux

 let t1= [1,3,5]           
let t2=[2,4,6]
> t1.concat(t2)         // T2 est collé ala fin de t1
[ 1, 3, 5, 2, 4, 6 ]
> 
> t1.includes(5)    // 5 est dans t1
true
> t1.includes(9)    // 9 n'est pas dans t1
false
> t1.indexOf(5)   //indice de 5 dans le tableau
2
> t1.indexOf(9)   // -1 car 9 n'y est pas et n'a donc pas d'indice
-1                // attention 0 est l'indice du premier élément
> t1.reverse()
[ 5, 3, 1 ]
> t1.join("#")     // recolle les élément du tableau pour former une chaîne et les séparant
'5#3#1'            // par la chaîne en argument
> t1.join(",")     // recoller en séparant par des virgules permet 
'5,3,1'           // de produit des fichier au format cvs d'excel
> t1.join("----")
'5----3----1'
> 
> 
> "1 6 7 9 3 6 2 7 9 8 33 1".split(" ").sort()
[
  '1',  '1', '2', '3',
  '33', '6', '6', '7',
  '7',  '8', '9', '9'
]
> 
let nombre="un deux trois quatre cinq six sept huit neuf"
let tab=nombre.split(" ");

tab.slice(2,6)


> tab.slice(2,6)
[ 'trois', 'quatre', 'cinq', 'six' ]  //de l'indice 2 a l'indice 5

>  tab                     // tab n'a pas changé
[
  'un',    'deux',
  'trois', 'quatre',
  'cinq',  'six',
  'sept',  'huit',
  'neuf'
]
> 
> tab.splice(2,6,"tree","four")
[ 'trois', 'quatre', 'cinq', 'six', 'sept', 'huit' ]
> tab
[ 'un', 'deux', 'tree', 'four', 'neuf' ]
> 

Attention:

slice ne modifie pas le tableau et ses arguments sont des indices de debut et de fin, il renvoie un nouveau tableaux. splice modifie le tableau ses argument sont l'indice de debut, le nombre d'argument a supprimer et les éventuels remplaçants. Il renvoie le tableau des éléments supprimés.

let t1= [1,3,5] 

> t1.pop()      // renvoi l'element le plus à droite
5
> t1
[ 1, 3 ]
3
> t1.push(99)  // ajoute a la fin
3
> t1
[ 1, 3, 99 ]
> 

> t1.shift()          // renvoi l'element le plus à gauche
1
> t1
[ 3, 99 ]
> t1.unshift(10)     //ajoute en tête
3
> t1
[ 10, 3, 99 ]
> 

On en verra plein d'autre par la suite.

Complément sur les fonctions

Les fonctions peuvent s'écrire avec deux syntaxes:

function f1(x){
  //faire des choses
  return x+1
}

ou

let f2= (x) => x+1 

La premiere est la syntaxe historique. Les deux se comportent différemment vis à vis de this.
Dans la seconde this est toujours global (window dans le navigateur).
On ne doit a-priori pas l'utiliser dans les méthodes (sauf si pas de this dans la méthode) . Dans la premiere si la fonction est une methode d'un objet, l'objet appelant est lié à this au moment de l'appel.

> let o1={v:3,a:function (x){return this.v+x}} 
undefined               
> o1.a(6)   //this est lié a o1
9

> let o2={v:3,a:(x)=>this.v+x}    
undefined                     
> o2.a(6) //this est toujours global
NaN

On peut définir et/ou appliquer des fonctions anonymes

> (x)=>(x+2)
[Function (anonymous)]
> > ((x)=>(x+2))(6)
8

Dont l’intérêt va apparaître juste après

Valeurs fonctionnelles dans des tableaux ou des objets

Rappelons qu'en javascript une fonction est une valeur comme une autre (on parle d'objet ou citoyen de premiere classe).

On peut définir :

let t = [ ( x=> x+1), 4 ,(x) => x.push(2)]
let peano = { succ :  (x)=>x+1 ,zero:0 }

Les fonctions que nous avons vu jusque là prenaient en argument, des valeurs de base des tableaux ou des objects mais jamais des fonctions.

En fait, les fonctions peuvent aussi être passées en argument ou être renvoyées par une fonction, on parle alors de fonction d'ordre supérieur

Fonction d'ordre supérieur

Certaines fonctions prennent naturellement des fonctions en arguments. Cela est fréquent en Mathématique ou l'on a des notations telles que la sommation

\[\Sigma_{i=a}^bf(i)\]

Cela se traduisent immédiatement en javascript si l’on peut utiliser des arguments fonctionnels

function somme (f,a,b){
  if (a>b) {return 0}
  else {return f(a) + somme (f,a+1,b)}
}

on peut alors définir la somme des entiers de 1 à 10

> somme (x=>x,1, 10)
55

ou la somme des carrés de 1 à 10

> somme ((x)=>x*x, 1,10)
385

ou le centième terme de la série mathématique \(\Sigma_{x=0}^{\infty} \frac{1}{2^x}\) qui converge assez rapidement vers 2.

> somme ((x)=>1/Math.pow(2,x),0, 100)
2

On dit que l'on a abstrait la fonction `f` en en faisant un paramètre (comme le sont déjà les bornes de sommation).  

On peut continuer à  abstraire.   

Si l'on veut la somme des carrés des nombres pairs.  On peut introduire en paramètre une fonction `suivant`


```js
function somme (f,a,b,suivant){
  if (a>b) {return 0}
  else {return f(a) + somme (f,suivant(a),b,suivant)}
}

Si on instancie suivantavec x=>x+1 on retombe dans le cas précédent

> somme(x=>x,1,10,x=>x+1)
55
> somme((x)=>x*x,1,10,x=>x+1)
385

Pour se limiter aux nombres pairs, il suffit de commencé sur un pair (par exemple 0) et d'instancier suivant avec x=>x+2

//somme des carres de nombre pairs entre 0 et 6
> somme((x)=>x*x,0,6,x=>x+2)
56

Notons qui bien que les fonctions précédentes soient définies de façons récursive, rien ne nous empêche de définir notre fonction somme avec une boucle:

function somme (f,a,b,suivant){
  let res=0;
  while (a<=b){ 
    res+=f(a);  
    a=suivant(a);
  }
  return res;
}

On peut aussi se servir de notre fonction somme pour des choses « un peu plus compliquées » comme approximer \(\Pi\).

La formule de Leibniz permet de calculer \(Pi/4\) est
\[\Pi/4 = 1/1 - 1/3 + 1/5 -1/7 + 1/9 -1/11 \dots \frac{(-1)^n}{(2n+1)} \dots\\ \ \ \ \ \ =\Sigma_{n=0}^\infty \frac{(-1)^n}{(2n+1)} \]

mais en réunissant chaque terme positif avec le terme consécutif négatif on arrive facilement à
\[ \Pi/8= 1/1*3 + 1/5*7 + 1/9*11 +... \frac{1}{n*(n+2)} \]

qui se traduit immédiatement en js


function appPi (a,b){
    return 8*somme((x)=>(1/(x*(x+2))),a,b,(x)=>x+4);
 }

> Math.PI-appPi(1,10000)
0.00019999999800024426
> 

On pourrait continuer en abstrayant aussi l'opérateur +, la valeur du cas non récursif etc.

Mais le principe est là, pour gagner en généralité, on abstrait en passant des fonctions en argument.

Avant de voir comment abstraire dans les parcours de tableaux, on peut donner un dernier exemple de math.

La méthode dichotomique

Si \(f\) est une fonction continue et monotone, on peut trouver un zero de \(f\) sur un intervalle \([a, b]\) par la méthode dichotomique quand f(a) et f(b) sont de signes opposés.

l’algorithme est le suivant:
\(\epsilon\) est la précision souhaitée.


function dichotomie (f,a,b,epsilon){
  if (Math.abs(b - a) < epsilon) {return a}
  else{
    let c = (a+b) / 2.0 

    let [na,nb] =  (f(a)*f(c)>0)?[c,b]:[a,c]
  return dichotomie (f,na,nb,epsilon)
  }
}

On peut utiliser cette fonction pour trouver une nouvelle approximation de \(\pi\) en le calculant comme zero de la fonction x=>cos(x/2)

> dichotomie ( x=>Math.cos (x/2.0),3.1,3.2,1e-10)
3.141592653561384
> 

Là encore on peut préférer écrire une fonction itérative avec une boucle:

function dichotomie2 (f,a,b,epsilon){
   while (Math.abs(b - a) > epsilon){
    let c = (a+b) / 2.0 
    let [na,nb] =  (f(a)*f(c)>0)?[c,b]:[a,c] 
    a=na;
    b=nb;   
  }
  return a
}

Appliquer une fonction à tout les elements d'un tableau

function map(t,f){
  let res=[];
  for (let i=0;i<t.length;i++){
    res.push(f(t[i]))
  }
  return res;
}

> map([3,4,5],(x)=>x*3)
[ 9, 12, 15 ]
> map([{nom:"Diderot",prenom:"Denis"},
     {nom:"Kant",prenom:"Emmanuel"},{nom:"Descartes",prenom:"René"}],
     x=>x.nom)

[ 'Diderot', 'Kant', 'Descartes' ]

Note

Cette fonction existe c'est une méthode du type Array:

> [1,2,3].map(x=>x*5)
[ 5, 10, 15 ]
> 

Il existe de nombreuse fonctions de ce type, définies comme méthode des tableaux.

Nous définirons nos propres version en exercice.

Fonction en résultat

Dans les exemple precedent on avait défini une fonction suivant qui donné le pas de progression (x=>x+1 , x=>x+4,....)

L'opérateur + attend 2 arguments, on peut définir une fonction plus permettant de fixer un des 2 arguments.

function plus(x) {
     return  function (y) {return y+x}}

plus(2) renvoie une fonction qui ajoutera 2 a son argument:

> let plus2=plus(2)
undefined
> typeof plus2
'function'
> plus2(6)
8

Fonctions en arguments et en résultat

L'exemple canonique est la composition de fonctions. C'est-à-dire la création d'une fonction compose qui combine 2 fonctions passées en argument pour former une nouvelle fonction.

// on compose les fonction f et g pour 
// former une nouvelle fonction.
function compose(f,g){
  return (x)=>f(g(x))
}

> let fcomp=compose(x=>x+2,x=>x*3)
undefined
> typeof fcomp
'function'
> fcomp(5)
17

//on peut évidemment l'appliquer directement
> compose(x=>x+2,x=>x*3)(7)
23

Si on suppose qu'on dispose d'une fonction mère qui associe sa mère à un individu. On peut définir alors définir une fonction grandmère (la mère de la mère).

grandmere=compose(mere,mere)

Si on cherche l'entier max des element pairs d'un tableau

let  maxPairs=
  compose(x=>Math.max(...x),
          x=>x.filter(x=>x%2===0))


> maxPairs([1,9,2,8,3,7,5])
8


//le chiffre le plus grand dans une chaine de chiffre
let res=(y)=>compose((x)=>Math.max(...x),
                     (x)=>x.split(""),y)


qres("458537")
8         

Coté Math, on peut calculer de façon approximative la dérivée \(f'\) d’une fonction \(f\) avec un petit intervalle \(dx\) de la manière suivante :

(si besoin page 1 de l'excellent)

function derive (f,dx) {
  return function (x){ 
      return  (f(x + dx) - f(x))/ dx
  }
}

let df=derive ((x)=>x*x,1e-10)
df(1)

On peut verifier que la dérivé de x*x est 2*x qui en 1 vaut bien 2, à l'approximation près.

> df(1)
2.000000165480742
> 

timers, asynchronisme, fonction de rappel et model d'execution

Un timer (anglicisme pour minuteur ) permet de déclencher une fonction apres un certain délais.

Javascript en propose 2. - setTimeOut (fct, delay): applique la fonction fct après delay millisecondes. - setInterval(fct,intervale) : applique la fonction fct tous les intervale millisecondes.

clearTimeout permet de stopper l'execution d'un setInterval (ou d'un setTimeout si le delay n'est pas écoulé)

La fonction passée en argument s'appel une fonction de rappel (callback).

//définissons une fonction pour avoir l'h au format h:m:s
function h(){
    let d = new Date();
    let  h = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();
    return h
}

//une fonction qui affiche `tic` suivi de l'heure 
function tic(){
    console.log("tic",h())
}

//test setTimeout
console.log("avant",h());
let t=setTimeout(tic,3000); //delais de 3 seconde
console.log("après",h());

qui affichera :

avant 15:27:33
après 15:27:33
tic 15:27:36               //affichage du tic 3 secondes APRÈS

Exemple avec un setInterval. On déclenche le tic toute les secondes. On utilise setTimeout pour arreter le setInterval par clearTimeout au bout de 10 secondes 10 millièmes

  let t1=setInterval(tic,1000);
  setTimeout(()=>clearTimeout(t1),10010)

Les choses se complique si le calcul en cours est plus long que le délais. Dans l'exemple ci-dessous on affiche l'heure, on planifie le lancement de tic dans 3 seconde avec setTimeout puis on lance un calcul qui prend plus de 3 secondes...

console.log("avant",h());
t=setTimeout(tic,3000); //délais de 3 seconde
for(let i=0;i<10000000000;i++){Math.sin(Math.pow(5555,2))}
console.log("après",h());

Voila le résultat...

```js
avant 15:33:20
après 15:33:30
tic 15:33:30 //le tic apparaît a 10 s

le tic n'est affiché qu'a la fin de l’exécution du code en cours, à savoir la boucle for qui prend environ 10 seconde () sur ma machine si la votre est plus rapide ajouter un 0...), puis le second console.log affiche l'heure puis lorsqu’il n'y a plus de code en cours, ticest exécuté.

Donc attention le délais des timers est un temps minimal qui peut augmenter en fonction du temps d’exécution du code en cours.

« Simple threading », concurrence, asynchronisme

Javascript est un langage « mono-thread ». Il exécute une instruction à la fois dans l'ordre ou elles arrivent. Mais il tourne dans un environnement hôte (le navigateur, nodejs, cordova...).

Lorsque

Compliquons un peu l'exemple précédent on crée un fichier "ex_concurrence1.js" avec le contenu suivant:

function h(){
    let d = new Date();
    let  h = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();
    return h
}

function longue(){
  for(let i=0;i<10000000000;i++){Math.sin(Math.pow(5555,2))}
}


function message(m){
  console.log(h()+" : "+m)
}

function un(m){
   message("debut un "+m)
   deux()
   message("fin un "+m )
}

function deux(){
   message("debut de deux")
  longue();
  message("fin de deux")
}


console.log("avant")
setTimeout(()=>{message("debut timer 1");longue();message("fin timer 1")},10);""; 
setTimeout(()=>{message(" timer 2")},1);""; 
un("premier")
setTimeout(()=>{message(" timer 3")},10);"" 
un("second")

produit

bash-3.2$ node ex_concurrence1.js 
avant
23:51:33 : debut un premier
23:51:33 : debut de deux
23:51:43 : fin de deux
23:51:43 : fin un premier
23:51:43 : debut un second
23:51:43 : debut de deux
23:51:53 : fin de deux
23:51:53 : fin un second
23:51:53 :  timer 2
23:51:53 : debut timer 1
23:52:27 : fin timer 1
23:52:27 :  timer 3

En fait l’environnent d'execution se compose de plusieurs chose.

une pile d'appel (des fonctions en cours d'execution), et une file d'attente des fonctions de rappel (callback queue) une **boucle d'événement et l'API de l’environnement hôte (web , node...).

au lancement du programme (qui est assimilé à une fonction principale main qui est empilée) - le console.log de la ligne 28 est placé sur le haut de la pille d'appel , s'execute, affiche avant et est dépilé. - puis le setTimeout de la ligne 29 est empilé, execute le premier setTimeout qui passe le relais a l'API, l’environnement gère le timing et à la fin du délais ajoutera la fonction de rappel dans à la fin la file d'attente;
l'instruction est dépilé - puis le setTimeout de la ligne 30 est empilé,execute le second setTimeout qui passe le relais a l'API, l’environnement gère le timing et à la fin du délais ajoute la fonction de rappel dans à la fin la file d'attente;
l'instruction est dépilé - puis l'appel à la fonction un est placé dans la pile d'appel et commence à être exécuté + puis l'appel à la fonction message est placé dans la pile d'appel et exécuté,
le message 23:51:33 : debut un premier est affiché;
l'appel à la fonction message est dépilé.

  |   deux()      |
  |   un(prmier)  |
  |   main()      |
   ---------------
* puis l'appel à la fonction `message` (dans `deux`) est empilé, puis exécuté,puis dépilée.
* puis l'appel à la fonction `longue` (dans `deux`) est empilé, puis exécuté,puis dépilée.
* puis l'appel à la seconde fonction `message` (dans `deux`) est empilé, puis exécuté,puis dépilée.
* `deux`a finie de s'executer est est dépilé, on peut continuer l'execution de `un`.