La programmation fonctionnelle

Voir la vidéo

La programmation fonctionnelle est un paradigme de programmation (au même titre que la programmation procédurale ou objet). De la même manière que la programmation objet est une évolution de la programmation procédurale, la programmation fonctionnelle est une évolution de la programmation objet. Celle ci peut être utilisée en complément de ces deux paradigmes.

Tout au long de cet article nous utiliserons des exemples en javascript. Même si ce n'est pas un langage de programmation fonctionnelle (au même titre qu'Erlang, Haskell, Elm, Elixir ou encore Scala) il intègre la pluspart des concepts de programmation fonctionnelle.

Les concepts et avantages

La programmation fonctionnelle repose sur plusieurs principes :

Immutabilité

Ce concept permet de s'assurer de la valeur des variables du début à la fin de nos actions. Cela va permettre d'avoir un code plus robuste et plus stable, donc moins de bugs et moins de maintenances. Tout au long de notre script les variables ne peuvent être modifiées.

/**
* Par défaut, on mute souvent les données
**/ 
let classe = ["Jean", "Marc"]
classe.push("Julie")
// Cette opération est une mutation, la variable originale est modifiée 
classe // ["Jean", "Marc", "Julie"]

/**
* En respectant l'immutabilité
**/
let classe = ["Jean", "Marc"]
let nouvelle_classe = [...classe, "Julie"]

Le principe d'immutabilité permet d'éviter les surprises en évitant les modifications implicites des variables.

L'utilisation de l'immutabilité peut avoir un effet négatif sur les performances en multipliant les données à stocker en mémoire. Pour limiter ces problèmes les langages de programmation peuvent sauvgarder les structure complexes (tableau, map, hash) comme un ensemble de référence (une sorte d'arbre).

let classe1 = [1, 2, 3..... 10000]
let classe2 = [...class1, 10001] 
// Ce second tableau ne contiendra pourrait conserver les référence du tableau précédent afin de ne pas générer 20001 nombres à sauvegarder ne mémoire

Le JavaScript est très peu adapté pour cette notion d'immutabilité mais des librairies comme ImmutableJS permet de combler ces besoins

Les fonctions pures

Une fonction pure est une fonction qui remplit les 2 conditions suivantes :

  • Le résultat de la fonction ne dépend que des arguments et pas du contexte extérieur
  • La fonction n'a pas d'effets de bords / d'effets secondaires
// Cette fonction est pure car elle n'a pas d'effet de bords et ne dépend de rien d'autre que ses arguments
let push = function (tableau, clef) {
    return [...tableau, clef]
}

// Ces fonctions ne sont pas pures, car le résultat varie et ne dépend pas des arguments
Date.now()
Math.rand()

Les fonctiones pures permettent d'avoir un code qui est thread safe, c'est à dire que l'on peut déléguer l'éxécution d'une fonction à un process séparé sans avoir à envoyer tout le contexte de notre application. Elles permettent aussi d'avoir un code qui est plus facilement testable gràce à une isolation naturelle.

Les fonctions d'ordre supérieur

Les fonctions d'ordre supérieur sont des fonctions qui possèdent au moins une des propriétés suivantes :

Avoir une ou plusieurs fonctions en paramètre

Vous avez sûrement déjà utilisé ce concept sans connaitre son nom. Voici un exemple :

let array = [1,2,3,4,5,6]

// retourne true si la variable est pair, false sinon.
let isEven = function(v) {
  return v % 2 == 0
}

// retourne [2, 4, 6]
let evens = array.filter(isEven)

On peut voir que le code est compact et très simple à lire. Voici l'équivalent en impératif :

let evens = []

for (let i=0; i < array.length; i++) {
  if (isEven(array[i])) {
      evens.push(array[i])
  }
}

On se rend compte que le code est beaucoup moins lisible dans ce deuxième cas et qu'il va être obligatoire d'écrire des commentaires pour simplifier sa lecture.

Renvoyer une fonction

Cette propriété permet de renvoyer une fonction au lieu d'une valeur. Voici un exemple :

function adder(value) {
    return function(inc) {
        return value + inc
    } 
}
let add10 = adder(10)
add10(5) // returns 15
[1, 2, 3].map(adder(20)) // [21, 22, 23]

Les lambdas

Les lambdas, aussi appelées fonctions anonymes, sont des fonctions utilisées de manière ponctuelle et n'effectuant généralement qu'une opération. Reprenons l'exemple de isEven().

// utilisation d'une lambda en JavaScript
let evens = array.filter(function(v) {
    return v % 2 == 0
})

// utilisation d'une lambda en CoffeeScript
let evens = array.filter((v) -> v % 2 == 0)

Certains langages, comme JavaScript ou PHP sont encore verbeux sur l'utilisation des lambdas. Java, Ruby ou Scala exploitent toute la puissance des lambdas pour avoir un code à la fois concis, clair et robuste.

Récursivité

Il est possible d'utiliser la programmation fonctionnelle de manière récursive.

"En informatique et en logique, une fonction ou plus généralement un algorithme qui contient un appel à elle-même est dite récursive." - Wikipedia

Cela va permettre d'avoir un code plus lisible et plus court. De plus, le code s'auto documenter de lui-même. Voici un exemple d'une fonction récursive.

let array = [1, 2, [3, 4], 5, [6, [7,8]]];

let addOne = function(v) {
    if(v instanceof Array) {
        // on va relancer addOne() pour 3, 4, 6, 7 et 8
        return v.map(addOne)
    }
    else {
        return v + 1
    }
}

// retourne [2, 3, [4, 5], 6, [7, [8, 9]]]
array.map(addOne)

Tail call

La récusrivité peut en revanche poser des problèmes dans certains cas. Par exemple

// Cette fonction est récursive (et ne sert à rien :D)
function decrease (n)  {
   if (n == 0) return 1
   return decrease(n-1)
}

decrease(100000) // Uncaught RangeError: Maximum call stack size exceeded

Le problème est que la profondeur d'éxécution est trop importante pour le langage qui comprend le code de la manière suivante.

appeller decrease(100000)
  appeller decrease(99999)
    appeller decrease(99998)
       ...
         appeller decrease(0)
         retourne 1
       ...
    retourne 1
  retourne 1
retourne 1

Les langage de programmation fonctionnelle dispose d'un cas particulier appeller tail call ou récursion terminale où, si la dernière instruction d'une fonction est un appel à cette dernière, alors la récursion peut être vue comme une itération.

appeller decrease(100000)
remplacer les arguments par (99999)
remplacer les arguments par (99998)
...
remplacer les arguments par (0)
retourne 1

Il faut donc bien réfléchir à l'écriture de vos fonctions récursives

// Je veux calculer la suite de fibonacci
function fib(n) {
  if (n <= 1) return n
  return fib(n-1) + fib(n - 2) // L'éxécution final n'est pas l'éxécution de la fonction mais une composition, tail call est donc impossible 
}

// Il faudrait l'écrire de la manière suivante
function fibWithTailCall(n, valeur_suivante = 1, accumulateur = 0){
  if (n === 0) return accumulateur
  return fibIterRecursive(n-1, valeur_suivante + accumulateur, valeur_suivante)
}

Map, Filter & Reduce

Map, Filter et Reduce sont des fonctions essentielles en programmation fonctionnelle qui permettent de remplacer la pluspart des problèmes que l'on résoud en programmation fonctionnelle avec des if ou des for.

map()

Cette méthode va permettre de transformer une liste. map() doit être utilisé de manière immutable (d'un objet A vers un objet B), c'est à dire que vous ne devez pas introduire d'effet de bord.

let users = [{
    name : 'John',
    lat : 48, 1,
    lng : -3,1
}, {
    ...
}]

// transforme un tableau d'objet User
// en un tableau d'objet Marker de Google Maps
// les coordonnées sont celles de chaque utilisateur
let markers = users.map(function(u) {
    return new google.maps.Marker({
        position : new google.maps.LatLng(u.lat, u.lng),
        map      : map,
        title    : u.name
    })
})

filter()

C'est une fonction que vous avez souvent vu dans cet article. Comme son nom l'indique, il prend en paramètre deux éléments :

  • un tableau
  • une fonction permettant de tester si la valeur correspond à notre filtre et doit être conservée.
let persons = [{
    name : 'John',
    age : 18
}, {
    name : 'Doe',
    age : 21
}, {
    name : 'Kevin',
    age : 15
}]

let isAdult = function(v) {
 return v.age >= 18
}
let adults = persons.filter(isAdult)

reduce

Cette méthode est un poil plus complexe et permet de transformer une liste au travers d'un accumulateur

let somme = [0, 1, 2, 3].reduce(function(accumulateur, nombre) {
  // A chaque itération, ce résultat sera sauvegarder dans l'accumulateur
  return nombre + accumulateur 
}, 0) 

let aplatir = function (arr) {
  return arr.reduce(function(acc, item) {
    return acc.concat(Array.isArray(item) ? aplatir(item) : item)
  }, [])
}
aplatir([[0, 1], [2, 3], [4, 5]]) // [0, 1, 2, 3, 4, 5]

Performances

Par contre, enchainer ces méthodes peut avoir un impact négatif sur les performances car le tableau est parcouru à plusieurs reprises.

// Je veux la moyenne des notes, que j'augmente de 3
let notes = [1, 8, 10, 20, 2]
notes
  .map(note => note + 3)
  .reduce((acc, note) => note + acc, 0)
  / notes.length // 11.2

Ici le map et le reduce oblige le parcours du tableau en double. Certains langages comme Elixir propose un système de Stream pour améliorer les performances :

1..5 |> Enum.map(&IO.inspect(&1)) |> Enum.take(1)
# 1 2 3 4 5
1..5 |> Stream.map(&IO.inspect(&1)) |> Enum.take(1)
# 1

Le stream permet de garder en mémoire les opération à effectuer pour transformer la liste, mais n'effectuera les opérations nécessaire qu'à la transformation.

La programmation fonctionnelle par langage

JavaScript

JavaScript utilise certains concept de programmation fonctionnelle depuis quelques temps déjà. Lorsque vous écoutez un évènement par exemple vous utilisez déjà les lambdas sans le savoir. En revanche c'est un langage qui reste basé en grande partie sur des mutations. Certains framework comme Redux impose une approche immutable qui est simplifié par la syntaxe ES6. Mais il est possible de pousser encore plus loin avec des librairies comme ImmutableJs

let eleve = {name: "Marc"}
// eleve.note = 18 pour faire cette opération sans muter
let new_eleve = Object.assign({}, eleve, {note: 18}) // Assez verbeux :(

// Avec immutablejs
let new_eleve = Immutable.fromJS(eleve).set('note', 18).toJS()

PHP

PHP possède aussi des notions de programmation fonctionnelle, cependant celles-ci sont moins bien intégrées dans le langage car beaucoup plus récentes. Il aura fallu attendre la version 5.3 de PHP pour enfin voir les fonctions anonymes apparaitre.

// en JavaScript
var array = [1,2,3,4,5,6];
array.filter(function(v) { return v%2 == 0; })
// en PHP
$array = [1,2,3,4,5,6];
array_filter($array, function(v) { return v%2 == 0; });

Ensuite, le chainage de fonctions rend la compréhension moins facile, alors que cela devrait être le contraire.

let array = [1,2,3,4,5,6];
// retourne les éléments pairs
// et applique une transformation qui les multiplie par eux même
// retourne [4, 16, 36]
array
  .filter(v => v % 2 == 0)
  .map( v => v * v )
$array = [1,2,3,4,5,6];
$square = function($v) {
    return $v * $v;
};
$isEven = function($v) {
    return $v%2 == 0
};

array_map($square, array_filter($array, $isEven));

On peut voir qu'en PHP, outre l'inconsistance de l'ordre des paramètres, il est nécessaire de lire de droite à gauche pour comprendre le mécanisme. La lecture est moins fluide, mais le résultat sera similaire.

Java

Java est un langage avec une orientation objet très forte. A ce titre, beaucoup de développeurs étaient réticents à l'import de la programmation fonctionnelle dans ce langage. Il apparait néanmoins timidement dans la version 8.

Android n'intégrant pas encore la version 8 de Java, la programmation fonctionnelle n'est pas encore prise en charge de manière officielle.

Scala

Scala répond au besoin des développeurs Java souhaitant avoir un langage fonctionnel. En ce sens, l'intégration de la programmation fonctionnelle est très poussée.

Swift

Swift est le nouveau langage de programmation d'Apple. Sa syntaxe étant plus légère qu'Objective-C, il intègre les composantes de programmation fonctionnelle contrairement à son ainé.

Ruby

Ruby intègre dans son langage les concepts de programmation fonctionnelle avec l'utilisation des Block, Proc et Lamba.

items = [1 , 2 , 3]
items_double = items.map do |x|
    X * 2
end

En revanche cela reste un langage en grande partie basé sur des mutations.

Publié
Technologies utilisées
Auteur :
Grafikart
Partager