Il y a de ces matins où je me réveille avec une idée en tête. J'ai envie de l'essayer tout de suite, mais la plupart du temps, je ne peux pas.
Spoiler : en général, à la fin je suis déçu (quand je ne l'oublie pas carrément). Cette fois j'ai été déçu, mais j'ai quand-même envie de le raconter.
Manipuler les tableaux en JS
Il y a une époque, dès que je voulais faire une opération sur un tableau, j'écrivais une bonne vieille boucle for, je parcourais les éléments, et je mettais un bloc de code qui faisait le travail.
Ça marche. Si c'est bien fait c'est efficace. Mais la plupart du temps c'est assez moche à relire.
Et puis lors d'un stage où j'ai passé pas mal de temps à écrire du JS, on m'a conseillé d'utiliser les méthodes d'itération sur les tableaux, telles que #forEach(), #map() ou encore #filter().
L'un des intérêts de ces fonctions, c'est qu'elles donnent du sens à l'opération qu'on effectue : quand je lis un forEach(), je sais que c'est pour effectuer un traitement sur la totalité du tableau ; quand je lis un map(), je sais que c'est pour construire un tableau à partir d'un autre, et ainsi de suite. Quand on combine ça aux first-class functions de JS, on a une bonne équipe.
L'idée
Le truc avec ces méthodes, c'est qu'au lieu de faire un parcours de boucle dans lequel on met un filtre (sous forme de if), puis des traitement, on va faire un parcours pour notre filtre (en appliquant une function avec la méthode filter()), un parcours pour effectuer un traitement (avec la méthode forEach()), et peut-être d'autres encore.
Je ne sais pas vous, mais moi il y a un matin où ça a commencé à me titiller.
Alors j'ai eu l'idée de créer un prototype (que j'ai appelé LazyArray) qui aurait les mêmes méthodes d'itération qu'Array ; Sauf que ces méthodes ne feraient rien du tout… enfin presque ! Exemple :
LazyArray.prototype.filter = function LazyArray_filter(fn, thisArg) {
this.queue.push({
type: TYPE.FILTER,
fn: fn,
thisArg: thisArg,
});
return this;
};
Comme vous l'avez peut-être compris, l'idée est de stocker les opérations effectuées dans une file d'attente, sans les effectuer ; le traitement sera délégué à une autre méthode, qui traitera tout dans une seule itération. J'ai choisi d'appeler cette méthode collect(), et j'ai commencé à implémenter quelques itérateurs :
LazyArray.prototype.collect = function LazyArray_collect() {
var elementIndex = 0;
var result = [];
var filtered;
var i, element;
var j, processor;
elementsLoop:
for (i = 0; i < this.array.length; ++i) {
element = this.array[i];
processorsLoop:
for (j = 0; j < this.queue.length; ++j) {
processor = this.queue[j];
switch (processor.type) {
case TYPE.EACH:
processor.fn.call(processor.thisArg, element, elementIndex, this.array);
break;
case TYPE.FILTER:
filtered = !processor.fn.call(processor.thisArg, element, elementIndex, this.array);
if (filtered) {
continue elementsLoop;
}
break;
case TYPE.MAP:
element = processor.fn.call(processor.thisArg, element, elementIndex, this.array);
break;
default:
break;
}
}
++elementIndex;
result.push(element);
}
delete this.array._lazy;
return result;
};
Cette méthode est assez lourde à lire ; en quelque sorte, elle se sacrifie pour que le reste du code puisse être plus « joli » (i.e. utiliser les méthodes d'itération) tout en effectuant un unique parcours de tableau. Comme vous le voyez, j'ai implémenté les itérateurs forEach(), filter() et map(). On peut les appeler comme ceci :
console.log(
[1, 2, 3, 4, 5, 6].lazy
.forEach(console.log)
.filter(isEven)
.forEach(console.log)
.map(addOne)
.collect();
);
Pour les curieux, le code complet est disponible sur GitHub.
Le résultat
Le résultat, c'est que le comportement est bien celui attendu : les itérateurs sont bien appliqués, dans le bon ordre et à peu près dans le même contexte qu'avec les méthodes natives. Par conséquent, j'ai voulu regarder, avant d'aller plus loin en implémentant d'autres itérateurs, si ça avait un impact sur les performances.
Hé bien figurez-vous que oui, ça a un bel impact ; mais dans le mauvais sens :
Ça fait un petit peu mal au cœur, mais c'est comme ça. J'ai improvisé une tentative d'explication dans une discussion sur Twitter :
@Amatewasu En effet. Je n'ai peut-être pas l'esprit clair, mais mon explication actuelle c'est que les parcours de tableau dans […]
@Amatewasu […] les fonctions natives du moteur JS sont incroyablement moins couteux que dans le code JS.
@Amatewasu Du coup, ça rendrait mes tentatives d'optim dans l'espace utilisateur complètement ringardes.
@Amatewasu Mais j'ai peut-être tout simplement raté quelque chose. J'ai fait ça vite fait, après une journée de boulot.
Guillaume Charmetant (@cGuille) February 12, 2015
Alors, à votre avis ?
Réactions
Je noterai ici les retours qui peuvent intéresser les lecteurs :
@cGuille lodash implémente le même principe http://filimanjaro.com/blog/2014/introducing-lazy-evaluation/
Youcef Mammar シ (@TKrugg) February 28, 2015