Guide de survie au royaume du fonctionnel - Partie I

Publié le 8 juin 2017 par Arnaud Morisset | architecture

Cet article est publié sous licence CC BY-NC-SA

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)).

Dans les grandes lignes

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.

Êtes-vous suffisamment pur(e)s ?

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.

High Order Functions

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 :

  • elle reçoit une ou plusieurs fonctions en tant qu’arguments
  • elle renvoie une fonction en guise de résultat

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]);

Second Order Function

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.

Conclusion

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.