Si vous avez l’habitude de trainer dans des lieux peu fréquentables tels que les meet-up de développeurs web, vous avez forcément entendu parler de la programmation fonctionnelle. Cette dernière est entrain de faire son petit bout de chemin tranquillement dans le monde du développement logiciel. Bien qu’elle soit présente depuis suffisamment longtemps pour que certaines personnes ne jurent que par elle, sa popularité dans le monde du développement web est, elle, beaucoup plus récente.
Vous êtes peut-être comme moi, un jeune développeur web élevé au grain et au Java, ou bien un vieux briscard ayant connu les débuts de l’orienté objet. Dans tous les cas, le premier contact avec le fonctionnel ne laisse pas indifférent. Si vous avez l’ambition de continuer dans cette voie et d’appliquer ce paradigme à votre développement de tous les jours, laissez-moi vous partager ce petit guide de survie qui fera office d’introduction.
Ce guide possède une approche pragmatique dont le but est de vous aider à incorporer des réflexes de programmation fonctionnelle dans vos projets habituels. En aucun cas, je prétends être un expert et, juste entre nous, je n’ai jamais compris ce qu’était une complexité en O(log(n)).
Vous l’avez peut-être déjà croisé au détour d’un couloir à la fac, souvent présente sous forme d’Ocaml ou autre Lisp. On en garde rarement un bon souvenir tant sa découverte fut enrobée d’une bonne couche de mathématiques, d’algorithmique fondamentale et appliquée, de … enfin bon, vous voyez le genre. Ce type d’enseignement traine une certaine réputation à la programmation fonctionnelle, on la voit souvent comme un outil pour matheux, réservée aux cadors de la computer science. C’est absolument faux et, si même une quiche en maths comme moi y arrive, vous pouvez vous y mettre.
De toutes façons, entre la tripotée de langages fonctionnels s’intégrant à un écosystème existant (Scala, Elixir, Elm, Rust, …) et les concepts fonctionnels qui s’ajoutent aux langages bien connus (arrivée des lambdas, des streams, etc…), vous ne pouvez plus passer à côté du paradigme fonctionnel. Une fois cette dure vérité acceptée, installez-vous confortablement, servez-vous un whisky, on passe aux conseils en pagaille.
Oui, je dis bien en pagaille, car je vais me contenter de vous présenter des concepts clés de ce paradigme. L’objectif n’étant pas de vous transformer en Functional God, mais juste de faire rentrer certains mécanismes dans vos habitudes orientées objets avant de faire le grand saut.
Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function.
John Carmack
Comme son nom le suppose, ce paradigme se base sur les fonctions. J’entends par là qu’elles sont au cœur de tout programme. Comme on dit dans le milieu, elles sont des citoyennes de première classe (first class citizen).
L’un des premiers concepts à appréhender est celui de fonctions “pures”. On entend par là qu’elles ne modifient pas l’état ni la valeur de données hors-contexte.
Je pense qu’un exemple rendrait ce point plus clair, faisons une fonction en JavaScript dont le rôle est de préparer un message de salutation.
// Fonction impure
function greeting() {
msg = "Hello " + firstname;
}
// Fonction pure
function greeting(firstname) {
return "Hello " + firstname;
}
Vous voyez la nuance ? Dans le premier cas, on utilise une variable qui se
trouve hors du contexte de notre fonction. On aurait donc pu changer sa
valeur au risque de créer un comportement imprévu dans la suite du programme.
Alors que dans le second cas, on manipule une copie obtenue via le passage en
paramètre. De plus, nous allons toujours préférer le fait de retourner une valeur
plutôt que de modifier une variable hors-contexte comme msg
dans le premier cas.
Vous l’aurez compris, en fonctionnel on n’aime pas les effets de bord et l’on a tendance à faire attention au contexte de toute donnée manipulable.
Un mécanisme un peu courant dans les habitudes des programmeurs, mais très usité dans la plupart des bibliothèques et des langages, est le concept de fonction d’ordre supérieur. Derrière ce nom bien pompeux se trouve en réalité une simple fonction répondant à l’un de ces deux critères :
Je vous propose un autre exemple (en Ocaml histoire de varier les plaisirs), la
création d’une fonction sum_fun
dont le but est de cumuler les résultats d’une
fonction appliquée aux éléments d’une liste :
(* val sum_fun : ('a -> int) -> 'a list -> int = <fun> *)
let rec sum_fun f = function
| [] -> 0
| x :: l -> f(x) + sum_fun f l ;;
(* val square : int -> int = <fun> *)
let square x = x * x ;;
(* - : int = 14 *)
sum_fun (square) [1; 2; 3] ;;
La fonction sum_fun
utilise un mécanisme que l’on appel pattern matching,
nous reviendrons plus en détail dessus lors d’un second article. Pour l’instant,
comprenez juste que cette fonction reçoit une fonction en paramètre et, que dans
le cas où il reste des éléments dans une liste, elle applique la fonction au premier
élément de la liste avant de s’appeler de manière récursive sur le reste de la liste.
Cela peut sembler obscur pour le moment, mais ce n’est trop grave. Dites-vous juste que ce concept a permis la création d’un autre que vous avez sûrement déjà croisé.
Allergique à l’Ocaml ? Voici un équivalent JS :
function sum_fun(f, list) {
if (list.length === 0) return 0;
else return f(list[0]) + sum_fun(f, list.slice(1));
}
const square = (x) => { return x * x; }
sum_fun(square, [1, 2, 3]);
Si je vous dit map
, filter
ou encore reduce
vous me dites ?
Ces fonctions bizarres qui viennent remplacer un
for
de temps à autres ?
Heu… oui, j’accepte cette réponse…
Mais ce sont surtout des fonctions de second ordre qui, dans leur implémentation, tirent parti du principe précédent afin de vous permettre de parcourir un ensemble d’éléments dans le but d’y appliquer un traitement. Vous retrouverez donc la logique de mon dernier exemple dans leur utilisation.
Imaginons que vous souhaitez appliquer un préfixe Synbioz_
sur toutes les chaînes
de caractères d’une liste. En bon Rubyistes, nous ferions un code du style :
names = ["Arnaud", "Vincent", "Quentin", "Victor"]
prefixed_names = names.map { |name| "Synbioz_#{name}" }
Ce type d’implémentation est commun à la plupart des langages depuis quelques temps,
car ces fonctions, que l’on peut qualifier de classiques dans le monde fonctionnel,
ont rejoint la plupart des langages (oui, même Java). C’est donc logiquement le premier
réflexe que vous devrez adopter peu importe votre techno. Tâcher d’utiliser ses fonctions
partout où vous les trouverez pertinentes en lieu et place de structure impératives car elles
vous permettent d’indiquer plus clairement votre intention. Le rôle de la fonction
filter
est assez explicite contrairement à une structure impérative plus classique
qui demandera une lecture plus minutieuse pour comprendre son rôle. L’utilisation
de ces fonctions peut aussi nous permettre d’éviter la création de variables inutiles
dont le rôle aurait été de stocker temporairement des données.
Il y a encore énormément à dire, mais je ne souhaite pas vous étouffer avec un surplus d’informations. On verra plus en détails le pattern matching, les lambdas et la récursivité dans le prochain article.
En attendant que pouvons-nous conclure de ce premier article ?
Ocaml ça a de la gueule, dommage que ça ne serve à rien. ԅ(≖‿≖ԅ)
Votre premier réflexe devrait être d’intégrer des mécanismes simples dans vos habitudes actuelles, pas de gober le paradigme d’un coup.
Vérifiez toujours s’il n’est pas plus pertinent d’utiliser une second order function plutôt qu’une structure impérative quand l’occasion se présente.
L’équipe Synbioz.
Libres d’être ensemble.