07 janvier 2016

Promesses et récursivité : ordonnancer des promesses simultanées

Lorsqu'on lance un groupe de promesses simultanées avec la méthode Promise.all(), l'on peut lier une instruction finale à leur accomplissement. Quand certaines de ces promesses de base dépendent des autres pour atteindre des objectifs intermédiaires, comme la définition d'un objet intermédiaire, il faut en répéter certaines selon la façon dont l'ensemble se synchronise.
L'enjeu est alors de reprendre, en la prolongeant, l'enrichissant ou la raffinant, la promesse itérative ".all()" à laquelle vient se lier l'instruction finale.

À noter - cette question est distincte de la restitution de l'ordre d'instanciation au fur et à mesure que les promesses se tiennent
- cf. HTML5 Rocks (archive) : JavaScript Promises: There and back again,
et Promesses : map().reduce() sur Equatorium.

Il s'agit ici d'attendre que les conditions intermédiaires soient remplies dans l'ensemble des promesses - bien que chacune soit a priori toujours tenue, certaines doivent être relancées en fonction de l'ensemble - pour enfin conclure la promesse globale initiale.

Vu sous l'angle mathématique des facteurs

Imaginons les cinq promesses simultanées "2", "3", "4", "6" et "8", composant l'argument itératif de ".all()" <>. Toutes seraient formellement tenues à l'issue de ".all()", mais toutes n'atteindraient pas leur objectif, qui serait de réunir certains des facteurs arithmétiques fournis (directement ou indirectement) par les autres promesses.

Les nombres 2, 3, 4 et 6 avec la suite de leurs facteurs

  • L'objectif des deux premières (nombres premiers) serait toujours atteint, indépendamment des autres.
  • Pour que l'objectif de la "4" le soit, il faudrait que celui de la promesse "2" l'ait déjà été.
  • Pour la "6", les objectifs de la "2" et de la "3".
  • Pour la "8", de la "4" directement et donc indirectement de la "2".

Trois promesses, les "4", "6" et "8", voient ainsi leur objectif se trouver en dépendance directe ou indirecte d'un facteur, d'une condition, l'objectif de la promesse "2".
Les objectifs "composés" se trouvent conditionnés par d'autres objectifs.

L'instruction finale de cet exemple résidant un affichage dès que tous les objectifs sont atteints : une fois que chaque nombre a réuni ses facteurs eux-mêmes regroupant leurs propres facteurs… (1)

En élargissant l'exemple jusqu'au "12" :

Le nombre 12 avec la suite de ses facteurs

Quand on ne peut rien ordonner

Que les exécutions aient lieu via des requêtes XMLHttpRequest ou en parcourant un objet (la nomenclature des clés ne garantit pas l'ordre d'itération), il est difficile de prévoir que la promesse du "2" sera "fulfilled" avant celle du "4" et que les objectifs seront traitables dans la priorité des conditions.

Certains cas (cf. "à noter" plus haut) autorisent la restitution d'une séquence ordonnée à l'issue de la tenue des promesses - l'ordre d'origine étant par exemple celui des index d'un tableau.
Mais si en amont, des traitements à conditions multiples peuvent se révéler difficiles à classer, ils ne seront pas du tout classables par une itération initiale elle-même non ordonnable - un objet…
Un classement, en outre, ne concernerait pas les traitements qui, indépendants (à objectif "premier"), doivent être traités "en parallèle" - comme "2" et "3".

Sur cet exemple des facteurs, et en les élargissant à 36 nombres dont 8 premiers (sur le mode d'une factorisation exhaustive, càd "2" comme facteur direct de "8"), j'ai développé un code simple <>.

Les nombres téléchargent leurs facteurs <> <>, soient 36 séquences de requêtes/callback. Une quinzaine de traitements plus tard, chaque nombre dispose de la suite complète de ses facteurs, en gigogne - la disposition des nombres étant à chaque fois mêlée, en fait la quantité de traitements supplémentaires oscille entre 5 et 40.
L'instruction finale est toujours synchronisée.

Les traitements nécessaires suivant les 36 séquences, s'exécutent de façon récursive à l'intérieur de la promesse de ".then()" <> liée à la promesse initiale qui itère les téléchargements ; ou bien ils peuvent avoir pour cadre une nouvelle promesse <>.

Ces cas seraient en l'occurrence facilement résolus par une itération indexable et restituable. Mais ils me servent de contrainte pour dégager facilement une structure avec la notion de facteur.

Les exemples sont sur equatorium.net et sur Github<>.

Récursivité

Après l'accomplissement des promesses de base, quelques objectifs sont tenus : au moins ceux de la "2" et de la "3" en gardant l'exemple des nombres et de leurs facteurs.
Quant aux autres, qui dépendent du contexte, tant qu'il en reste à atteindre, je vais relancer la fonction qui retourne la promesse initiale, et/ou qui lui est liée par la méthode ".then()".

Relancer la promesse initiale

Une manière de faire : auto-chaîner la promesse initiale. La fonction se paramètre elle-même comme argument de la méthode "then()" qu'elle retourne :

var promRecursive = function () {
 if (…) {
  return Promise.all( //promesse initiale
   datas.map(function (value, ind) {
    return new Promise(function (resolve) { … }); //promesses de base groupées
   })
  )
    //la fonction se paramètre elle-même comme argument :
  .then(promRecursive);
 }
}

promRecursive() //promesse initiale et récursive
.then(function () { //instruction finale
 (…)
});

Cf. Ordonnancer un groupe de promesses simultanées <>

La condition "if (…)" sert à évaluer la progression des objectifs. Avec l'exemple des nombres "2", "3", "4", "6" et "8" :

var promRecursive = function () {
 if (datas.proxy.length > 0) {

Cf. Ordonnancer un groupe de promesses simultanées (1) <>

"datas.proxy" <> est un tableau qui stocke les nombres n'ayant pas tenu l'objectif de réunir leurs facteurs ; il est "splicé" <> au fur et à mesure qu'ils le font.

Relancer des instructions en les liant à la promesse initiale

Après la première itération, dans la plupart des cas, les instructions devraient toutefois être exécutées de façon synchrone, sans nouveau recours aux promesses. Des données d'API n'auraient pas vocation à être téléchargées plusieurs fois de suite.
L'appel de la fonction est alors simplement récursif, une fois paramétrée dans ".then()" :

promRecursive = function () {
 if (datas.proxy.length == 5) { //première itération
  return Promise.all(
   datas.proxy.map(function (values, ind) { … })
  )
    //la fonction s'invoque elle-même comme argument :
  .then(promRecursive);
 }
 //itérations suivantes tant qu'il y a des objectifs non atteints :
 else if (datas.proxy.length > 0) {
  datas.proxy.forEach(function (values, ind) { … });
     //par récursivité :
  promRecursive();
 }
};

Cf. Ordonnancer un groupe de promesses simultanées (2) <>

Comme avec les versions précédentes, l'on obtient en conclusion chaque nombre accompagné de la suite de ses facteurs directs et indirects :
{"2":[],"3":[],"4":[2,[]],"6":[2,[],3,[]],"8":[4,[2,[]]]}

Avec l'exemple des 36 nombres - tous facteurs directs d'office, téléchargements - développé sur ordonnancer un flux de téléchargements simultanés (1) <>, voici un extrait :

Le nombre 1386 avec la suite de ses facteurs

Les relances sont toujours encadrées par une promesse

Ainsi, dans ces deux exemples, la promesse initiale est prolongée d'abord en se liant à elle-même, comme précédemment, puis de manière simplement récursive, dans le cadre ainsi de ".then()", jusqu'à ce que chaque objectif des promesses de base soit atteint…

Laisser les conditions évoluer jusqu'à ce que toutes soient réunies pour l'atteinte de l'objectif qu'elles contraignent le plus.

Cela marche également avec des fonctions plutôt que des conditions :

promInit = function () {
 return Promise.all(
  datas.proxy.map(function (values, ind) { … })
 )
}
promRecursive = function () {
 if (datas.proxy.length > 0) {
  datas.proxy.forEach(function (values, ind) { … });
  return promRecursive();
 }
}

promInit()
.then(promRecursive)
.then(function () { //instruction finale
 (…)
});

Cf. Ordonnancer un groupe de promesses simultanées (3) <>

Comme les récursions s'exécutent dans le cadre de "then()", elles sont encore encadrées par une promesse liée à la promesse initiale, à laquelle l'instruction finale est synchronisée :
(
    .all(promesses de base) //promesse initiale
    .then(récursions d'instructions simples) //prolongation
)
.then(instruction finale)
(2)

Coordination progressive des résultats

Il est donc aisé de coordonner les résultats d'un flux de promesses simultanées, via un aiguillage récursif qui en prolonge la promesse initiale.

La promesse initiale se trouve en effet rallongée jusqu'à ce que chaque objectif des promesses de base soit atteint, au-delà de sa résolution. L'instruction finale est alors de fait liée à une promesse initiale augmentée.

La récursivité laisse se fonder progressivement une coordination - un agencement des conditions - dans le résultat de la promesse initiale ".all()".

Dès qu'est réduit à zéro le nombre d'objectifs à atteindre (le tableau "datas.proxy" dans l'exemple), la promesse initiale se conclut. C'est là que l'instruction finale s'exécute :

promRecursive()
.then(function () { //instruction finale
 (…)
});

Note : par ailleurs, face à des objectifs interdépendants, à des conditions réciproques entre objectifs, la récursivité serait filtrée par une structure conditionnelle totalement différente.

Avec des conditions indirectes

Dans l'exemple des 36 nombres, les promesses sont renseignées sur toutes celles dont elle dépendent.
Dans celui des 8 nombres, ce n'est pas le cas, avec des facteurs indirects : une génération de "conditions induites" pour la "8" avec une dépendance à la "2" via la "4".

Quel que soit le nombre de facteurs indirects, quelle que soit la mesure entre conditions directes et indirectes, quel que soit le nombre de générations induites en dépendance logique <>, cela fonctionne tout aussi bien :
ordonnancer une "généalogie" dans un groupe de promesses simultanées (1) <>.

(1) Une condition quelconque existe en première génération, en première et nièmes générations, ou bien derrière la première génération.

(2) Les autres cas :
(récursions de promesses)
.then(instruction finale)
et :
.all(promesses de base)
.then(récursions d'instructions simples)
.then(instruction finale)

Aucun commentaire: