Entre les évènements de souris et de clavier, les callbacks, les promesses, les itérateurs et les streams le petit monde du JS offre beaucoup de modèle de programmation qui sont tous proches mais elle ne sont pas identique et surtout elles ne sont pas toutes compatibles. Ce qui veut dire qu’enchainer les unes derrière les autres ca peut vite devennir l’enfer.
Je regarde beaucoup de conférences, la plus part du temps sur InfoQ.
L’autre jour j’ai vu une conférence conjointe entre un gars de
Microsoft et un autre de Netflix
(Reactive programming).
Ils présentaient un framework qui permet de traiter tous les types d’événements
de la même manière. Que ce soit des clics de souris, des touches de clavier,
des requêtes asynchrones ou de la traversée de liste, ils proposent de travailler
ces évènements avec des opérateurs que l’on est habitué de voir sur des listes,
genre find
, map
, etc.
Pour ce faire on wrap nos sources d’évènements avec les objets de l’api pour standardiser l’api avec laquelle on va consommer ces évènements. D’après ce qui est dit dans la présentation, en arrière l’api crée un mix entre les patterns [observateur](http://fr.wikipedia.org/wiki/Observateur_(patron_de_conception) et itérateur. C’est ce qu’ils appellent le Functional Reactive Programming.
Ils ont mis en place ces idées dans une libraire. Puis cette librairie a été porté dans différent langage, comme en atteste leur compte github. Comme c’est Microsoft qui est en arrière de ce projet l’api qui est proposé utilise des noms à la LINQ.
J’ai commencé à lire la documentation disponible pour le portage JS de cette libraire. Puis j’ai cherché d’autres sources d’info plus détaillées. Et je suis tombé sur Highland.js de @caolan qui est aussi l’auteur de Async.js.
Grossièrement l’idée est la même que pour RxJs. Sauf qu’il y arrive par une logique de réunification. De ce fait là, Highland.js propose une api qui supporte plein de sources d’évènement.
Les sources supportées sont:
Parmi les sources d’inspiration @caolan mentionne lowdash et underscore.
J’avais un feeling plus agréable avec Highland.js qu’avec Rx.js. Alors j’ai changé d’idée et j’ai commencé à jouer avec Highland.
On comprend toujours mieux avec les mains dans le cambouis. Alors je me suis fait un projet jouet ici.
Le layout, très simple. Un mini script bash pour:
Droit au but, jetez un oeil à ceci
highland('keyup',$('#fld'))
.debounce(1000)
.latest()
.map(function(event){ return encodeURIComponent($('#fld').val()); })
.map(function(encodedSearchTerms){ return highland($.ajax("search.html?param="+encodedSearchTerms)); })
.merge()
.errors(function(err, push){ push(null, {}); })
.each(function(result){ $('#content').html(result) })
Le processus est super simple, une fois que l’on a compris le principe. Il y a trois types d’opérateurs avec highland:
highland(whatEver)
.each(function(){})
`.map()
Chaque appel chainé va ajouter une opération au pipeline, sauf le dernier. Comme le dernier appel est un apirateur, il va aspirer des valeurs immédiatement. Les opérations intermédiaires sont lazy.
Donc ce que l’on fait dans ce bloc de code:
keyup
du champ #fld
Écrire cela la première fois est loin d’être facile. Par contre, je trouve le résultat plutôt agréable. De plus, j’ai choisi de faire mes tests avec du js juste pour simplifier la loop de feedback. Mais avec une syntaxe coffeescript ça devient fou:
highland 'keyup',$('#fld')
.debounce 1000
.latest()
.map (event)-> encodeURIComponent($('#fld').val())
.map (encodedSearchTerms)-> highland($.ajax("search.html?param="+encodedSearchTerms))
.merge()
.errors (err, push)-> push(null, {})
.each (result)-> $('#content').html(result)
On avance un peu. On va regarder ici
var counter = 1;
var click1 = highland('click',$('button#btn1'));
var click2 = highland('click',$('button#btn2'));
highland([click1,click2])
.merge()
.each(function(){
$('#score-board').html("<p>" + counter++ +"</p>");
});
Ici on travaille avec deux sources asynchrones. Les deux évènements clic peuvent se produire en même temps ou l’un après l’autre. Peu importe. Les deux streams qui wrap les sources de clic sont mergé ensemble.
C’est comme avoir un Y en plomberie. On fusionne deux sources en une seule.
Et enfin le .each()
permet de consommer le nouveau flux et de travailler avec ses valeurs.
On avait un merge, on va travailler avec un fork qui fait basiquement l’inverse. On part d’un flux que l’on va diviser en deux.
Mais un fork ne va pas séparer les évènements, les deux stream vont avoir le même contenu. De plus pour être certain que personne ne manque de contenu, tous les flux qui viennent d’un fork s’attendent entre eux pour vraiment commencer à consommer leur source.
var data = [10,20,40,50,80,10,20,304,204,432,432,432,432,432,432121,543,543,5432523,432,321,321,432654,654,765,231543,543765,765432,543]
var s = highland(data)
var s1 = s.fork();
var s2 = s.fork();
s1.filter(function(x){ return x % 2 })
.consume(function(err, x, push, next){
if(x === highland.nil){
push(null,x);
}else{
console.log("even says"+x);
push(null,x);
next();
}
}).resume();
console.log("s1 is started")
s2.reject(function(x){ return x % 2})
.consume(function(err, x, push, next){
if(x === highland.nil){
push(null,x);
}else{
console.log("odd says"+x);
push(null,x);
next();
}
}).resume();
console.log("s2 is started")
Ici un crée deux copies du flux initial. Pour l’un on va filtrer les nombres pairs,
pour l’autre les impaires(on rejette les pairs du flux).
Puis pour chacun on va préparer un .consume()
.
Contrairement au .each()
, .consume()
n’aspire pas immédiatement les valeurs.
De plus .consume()
offre plus de souplesse, par exemple on peut rejeter
une valeur du stream. En contrepartie, la fonction que l’on passe à
.consume()
est pas mal plus complexe.
On découvre ici le higland.nil
, c’est un peut l’équivalent de
la liste vide en LISP, c’est un marqueur de fin
de stream.
.consume()
reçoit aussi deux fonction, push
et next
. push
permet de
remettre une valeur dans le pipe alors que next
permet de déclarer que le traitement
du flux d’entrée est terminé.
Pour l’instant, je n’ai pas vu de problème avec mon utilisation. D’après ce que j’ai vu il y a un petite liste de demande sur le bugtracker, mais ça à plus l’air d’être des propositions d’amélioration. C’est donc plutôt bon signe.
À ce jour il me semble que le code produit avec cette librairie est assez clair et relativement puissant. Reste à voir si en situation réelle on pense à sortir highland.js de notre utility belt.
La prochaine fois que je touche à Highland.js je vais essayer de mieux comprendre le concept en arrière du back-pressure dont il est question dans la documentation.
Pour l’instnant je ne l’ai travailler que dans le browser, mais highland.js est prévu pour fonctionner aussi dans node. Donc je voudrais aussi voir ce que cela donne dans ce contexte la.