Parmi les sujets qui nous ont occupés en 2010, je retiendrai une fois de plus l'intérêt croissant des grands du Web pour la Programmation Fonctionnelle (Scala, F#...). Un paradigme qui, s'il est bien appliqué, permet d'avoir un code complètement réutilisable grâce aux objets immutables (pas de modification ni de partage d'état), aux fonctions en tant que type (String, Integer et maintenant Function)... mais aussi de construire des applications hautement disponibles. Pour une introduction de la programmation fonctionnelle, je vous renvoie vers l'excellent article sur haskell.org.
Mais ce ne sont pas que les grands du Web qui en auront besoin, en effet nos architectures orientée web ou WOA (Web Oriented Architecture) vont aussi avoir besoin de la programmation fonctionnelle et cette vidéo nous le prouve. De plus en plus de services sont disponibles sur internet (accessibles notamment via REST) et créer ou transformer une donnée avant de l'envoyer en tant que paramètre de service va être une habitude à prendre très rapidement. Et en effet, la transformation d'un format de données vers un autre est aussi une des forces de la programmation fonctionnelle.
Une question se pose alors : mais comment va t-on faire pour apprendre à écrire fonctionnellement et ainsi être prêt quand ces langages fonctionnelles auront balayé des entreprises Java et Ruby (XD) ?
Pas de panique et surtout pas la peine de passer les 6 prochains mois à apprendre et maitriser ces langages sur votre temps de sommeil, la solution tient en un mot : Javascript !
Nous allons voir comment écrire un code de manière fonctionnelle en Javascript ou plutôt comment transformer un code Javascript de tous les jours en code Javascript fonctionnelle. Et pour répondre à cette question, cette article s'appuiera sur quelques exemples rencontrés sur le terrain et ainsi vous prouver que dès maintenant, dans vos applications d'entreprise, vous pouvez vous entraîner à programmer de manière fonctionnelle et ne pas vous apitoyer sur votre sort car vous ne faîtes pas de Scala ;-)
Car oui, même si Javascript est sur le papier un langage objet à prototype, vous allez quand même pouvoir coder de manière fonctionnelle. En effet, les pré-requis à un langage fonctionnelle sont respectés : pouvoir avoir une fonction comme type de classe et ainsi pouvoir passer des fonctions en paramètre de méthode.
Itération (map, fold, some...)
Voici un premier exemple de validation d'un champ. Ce code vérifie que le protocole fourni est bien soit http soit ftp, la liste n'étant pas arrêtée.
Bien évidemment, je ne triche pas et je reprends tel que le code original sans optimisation et pour cause : c'est très souvent ce type de code que l'on retrouve chez nos clients.
La première fonction est la version originale et la seconde est la version orientée fonctionnelle.
/** Before fp. */
function validProtocol(obj) {
var result = false;
if(obj == null) {
return result;
}
var availableProtocols = ["http", "ftp"];
for(var i=0; i<availableProtocols.length; i++) {
if(obj.length >= availableProtocols[i].length) {
var protocolLength = availableProtocols[i].length;
var toTest = obj.substring(0,protocolLength);
if(toTest == availableProtocols[i]) {
return true;
}
}
}
return result;
}
/** After fp. */
function validProtocol(path) {
if($.isEmpty(path)) return false;
return some("'"+path+"'.startsWith(_)", ["http", "ftp"]);
}
On note ici plusieurs choses :
- Plusieurs fonctions ont été introduites, c'est le principe des méthodes composées :
- chaque méthode n'a qu'un seul rôle : isEmpty ne vérifie que si la chaîne est vide ou null et ne fait rien d'autre !
- chaque méthode n'a qu'un seul niveau d'abstraction : accès disque, sauvegarde en base, calcul du retour... dans une seule méthode ne serait vraiment pas une bonne idée ! Les niveaux d'abstraction seraient ici différents donc devraient être dans plusieurs méthodes.
- On passe donc d'un code qui fait tout à un code qui fait appel à d'autres fonctions génériques ayant un rôle bien précis et retournant les résultats attendus.
- Ainsi, et en exagérant un tout petit peu
, on se retrouverait presque à lire des phrases (à la manière d'un DSL), toutes les abstractions ayant été redescendues (test de non nullité, parcours de la liste...) dans des fonctions ayant des noms explicites. - De nombreuses fonctions utilitaires au niveau des tableaux ou des listes existent déjà donc ne réinventez pas la roue : utilisez functionnal javascript ou underscore.js ! (ici functionnal javascript)
- Et au final on obtient un code réellement réutilisable avec des fonctions comme isEmpty, some...
Ce n'est bien sûr pas une solution ultime mais on voit ressortir ces fonctions très prisées en programmation fonctionnelle à savoir les map, fold, reduce, some... qui sont des fonctions nous cachant la récursion sur la liste : tout ce qu'ellent nous demandent c'est la fonction à exécuter sur chaque élément de la liste, map faisant une transformation, fold et reduce des réductions, some vérifiant si au moins un élément vérifie la fonction passée...
Maybe et Either
Deuxième exemple avec cette fois-ci uniquement le code final qui doit respecter la demande suivante :
- si le produit est autorisé :
- si l'utilisateur est logué, alors on lui affiche une image avec un bouton pour effectuer une action
- sinon, on ne lui affiche qu'une image
- sinon :
- si l'utilisateur est logué, on lui affiche un bouton standard pour effectuer une action
- sinon on n'affiche rien
Vous êtes quasiment face à la spécification demandée et voici le code final :
function newAllowedButton(container, theId, theUrl, isAuthenticated) {
var image = image("100%","100%","picture.png","Allowed");
var imageWithButton = richLink(image("100%","100%","picture.png","Allowed"), template(URL_UPDATE).withParam({id:theId, url:theUrl}));
var button = richLink(image("100%","100%","picture.png","Allowed"), template(URL_UPDATE).with({id:theId, url:theUrl}));
var isAuthenticatedFunc = function() { return isAuthenticated };
var component = either(
either(imageWithButton, image, isAuthenticatedFunc),
maybe(button, isAuthenticatedFunc),
isAllowed(theId)
)();
if(component) $('#'+container).append(component.content);
}
Les premières lignes de la fonction créent les différents composants dont elle aura besoin. Un petit shortcut est ensuite créer au niveau du booléen isAuthenticated afin de la wrapper dans une fonction et de la mettre dans une variable ayant un nom explicite (isAuthenticatedFunc). Et enfin, comme si nous lisions la spécification, nous sommes face aux fameuses fonctions maybe et either mais cette fois-ci en Javascript !
Ainsi either, qui prend en paramètre une action 1, une action 2 et une fonction à tester, exécutera l'action 1 si la fonction en 3ème argument renvoie true et exécutera l'action 2 si elle renvoie false. Tandis que maybe se contentera juste d'exécuter son action passée en 1er argument si l'exécution de la fonction passée en 2ème argument renvoie true, et ne fera rien si elle renvoie false.
Ces fonctions se codent très facilement, si toutefois une librairie plus haut niveau fournit ces implémentations, merci de le préciser en commentaire ! Si besoin, voici nos either et maybe, celles-ci pouvant prendre en paramètre une fonction à tester pouvant avoir 0 ou 1 paramètre (et sera répercuté sur l'action) mais bien sûr à vous d'adapter à votre besoin :
/** Execute action if predicate is true. */
function maybe(action,predicate){
return function (val) {
if(!val) {
if((!predicate)||(predicate())) return action();
else return null;
} else {
if((!predicate)||(predicate(val))) return action(val);
else return null;
}
};
}
/** Execute action1 if predicate is true and execute action2 if predicate is false. */
function either(action1,action2,selector) {
return function (val) {
if(!val) {
if((!selector)||selector()) return action1();
else return action2();
} else {
if((!selector)||selector(val)) return action1(val);
else return action2(val);
}
};
}
De même, vous avez peut-être noté la ligne template(URL_UPDATE).withParam({id:theId, url:theUrl}) qui fait appel à une fonction de templating que nous avons écrit afin de ne pas avoir à faire des appels à la méthode replace directement sur nos chaînes de caractères. Encore une fois, nous descendons l'abstraction et nous avons une API bien plus simple à utiliser et nous obtenons un code bien plus lisible.
Voici les implémentations, elle s'appuie sur json-template :
template("/an/url.html?id={id}&&url={url}").withParam({id:anId, url:anUrl})
/** Template. */
function template(t){
return ({ withParam: function(obj) {
return jsontemplate.Template(t).expand(obj);
}});
}
/** From template. */
function fromTemplate(t) {
return function(o) {
return jsontemplate.Template(t).expand(o);
};
}
Nommage, transformation et simplicité
Enfin, un exemple un peu plus compliqué. Je précise qu'il n'est pas important de comprendre l'exemple mais plutôt le refactoring effectué pour obtenir un code plus orienté fonctionnel.
Regardez bien tous les tests, toutes les transformations en nouvelles listes, toutes les boucles effectuées... le tout imbriqué les uns dans les autres rendant à la fois la lecture et le refactoring presque impossible (avant et après) :
/** Before fp. */
function newBeforeAfterLabel(container, id, list, type) {
var idList = new Array();
for(var index in list) {
idList.push(list[index].id);
}
var xml = ajax.get(contextualize("/types/"+idList.join(",")));
if (xml.status != 404 && xml.responseText) {
var resultList = $.evalJSON(xml.responseText).list;
var txt='';
if(type == "BEFORE"){
if(resultList.length == 0 || resultList.length ==1) {
txt = 'Bla 1';
} else {
var printList = new Array();
for(var i in resultList) {
if(resultList[i].id != id)
printList.push(resultList[i].name);
}
txt = 'Bla 2 : ' + printList.join("<br />") + ")";
}
} else {
if(list.length !=0) {
if(resultList.length == 1) {
txt = 'Bla bla 3 : ' + list[0].name + ")";
} else {
var printList = new Array();
for(var i in resultList){
printList.push(resultList[i].name);
}
txt = 'Bla 4 : ' + printList.join("<br />") + ")";
}
} else {
txt = 'Bla 5';
}
}
$('#'+container).append('<span>'+txt+'</span>');
}
}
/** After fp. */
function newBeforeAfterLabel(container, id) {
var data = getDataForId(id)
if(data){
var type = data.type
var list = data.list
var before = type == "BEFORE";
var after = type == "AFTER";
if(before || after) {
var idList = map('_.id', list);
var xml = ajax.get(contextualize("/an/url/"+idList.join(",")));
if (xml.status != 404 && xml.responseText) {
var resultList = $.evalJSON(xml.responseText).list;
var resultListWithoutMe = map('_.name', filter('_.id!='+id, list));
var resultListWithoutMeInHtml = resultListWithoutMe.join("<br />");
var txt='';
if( before && resultListWithoutMe.length <= 0) {
txt = 'Bla 1';
} else if(before && resultListWithoutMe.length > 0) {
txt = 'Bla 2 : ' + referentsWithoutMeInHtml + ")";
} else if(after && resultListWithoutMe.length > 0) {
txt = 'Bla 3' + (filter('_.id!='+id,list).length > 1 ? ' a ':' b ') +resultListWithoutMeInHtml + ")";
} else if(after && resultListWithoutMe.length == 0){
txt = 'Bla 4';
}
$('#'+container).append('<span>'+txt+'</span>');
}
}
}
}
La chose qui saute aux yeux immédiatement est qu'il n'y a désormais que très peu de niveaux d'imbrication et plus aucun if dans un for dans un if dans un for (bonjour la complexité cyclomatique...).
On remarque aussi que la transformation d'une liste d'objets vers la liste des identifiants de ces objets est devenue très simple et très lisible : var idList = map('_.id', list). Une ligne concise et auto-documentée pour peu que tout le monde utilise la même librairie fonctionnelle. Mais même sans, on devine très bien ce que le code fait.
Enfin, quelques bonnes pratiques :
- Remontez le plus possible les tests effectués et stockez les dans des variables, vos if deviendront plus lisibles !
- Usez et abusez (avec modération) des fonctions de transformations de liste : map('_.name', filter('_.id!='+id, list)) => filtrage dans la liste de tous les éléments sauf celui d'identifiant id et transformation de cette liste d'objet en liste de name.
- Des que vous mélangez plusieurs abstractions, faîtes des fonctions ! Ici, getDataForId en est une : elle récupère un objet à partir d'un id, l'appel REST, l'Url... n'est pas mon problème dans cette méthode qui n'a pour but que de créer le widget before after label.
Conclusion
Bien sûr, pour tous ces exemples, d'autres solutions existent et de nombreux plugins jQuery doivent faire le travail.
L'idée ici n'est pas d'obtenir la solution parfaite au problème donnée mais bien de montrer comment transformer un code objet/procédural en un code plus fonctionnel. Des librairies comme jQuery, functionnal javascript ou bien encore underscore.js vous aiderons grandement dans cet apprentissage.
J'espère vous avoir convaincu de faire de la programmation orientée fonctionnelle en Javascript !




Bonjour !
L'article semble super interessant, mais le design coupe complétement les
exemples de code, les rendant illisible. Même la textarea de commentaire
coupe le contenu du commentaire écrit.
C'est dommage, parce que le contenu est de qualité !
Merci pour tes remarques, j'ai corrigé le commentaire et réduit un tout petit peu la taille des snippets de codes.
Cela reste lisible et surtout cela les rend exploitables !
Super, c'est bien plus lisible, même si la typo est petite, au pire on peut zoomer.
Je me demande si tu ne devrais pas refléchir à agrandir quand même la width de ton contenu ? C'est toi le chef.
En tout cas je suis surpris qu'un blog avec des articles aussi interessant ait si peu de personnes commentant.
Je ne fais pas beaucoup de JS mais en PHP je m'efforce de programmer de cette façon par simplicité. En gros je fais des fonctions de basse couche qui n'ont qu'un seul but, puis dans mon code j'en fais appel au besoin. Suivant la complexité de l'application je fais parfois plusieurs niveaux fonctionnels.
Le plus bas niveau d'un objet, exemple :
$-user->get($id);
un niveau au dessus :
$user->getIn($id,$fn_callback);
la méthode getIn() utilise $this->get() pour récupérer l'user (return false le cas échéant) puis passe l'objet user dans $fn_callback
etc
Du coup dans l'application je ne me prends plus la tête à faire des moulinette, j'utilise ce qui est déjà fais.
Je ne savais pas que c'était une certaine forme de programmation fonctionnelle.