Programmation fonctionnelle

Qu'est-ce que c'est ?

Pourquoi en avons nous besoin ?

Par Vivian Pennel / @vp3n
Lead Developer chez IOcean

I - Historique

En quelques mots

Les langages fonctionnels existent depuis toujours ou presque

Ils reviennent sur le devant de la scène, mais pas forcément dans le sens "pur" du terme

Pourquoi ce paradigme n'a pas été plus utilisé plus tôt ?

Phase 1 : obstacles techniques

  • Puissance des machines
  • Utilisation mémoire
  • Processeur mono-coeur

Phase 2 : scientifique et académique

  • Application dans la recherche
  • Utilisé pour dans l'enseignement

Phase 3 : application industrielle spécifique

  • Programmation concurrente et distribuée
  • Fault-tolerant systems

Phase 4 : Renaissance

  • Contraintes de scalabilité de plus en plus forte (Google, Twitter ..)
  • Multi-coeurs, parallèlisme
  • Introduction des concepts dans les langages impératifs les plus utilisés
  • Montée en puissance de langages fonctionnels (Scala, Erlang, Clojure ...)

II. La programmation fonctionnelle

c'est le truc avec les parenthèses partout?

Les grand principes

La fonction comme base de travail (Lambda)

  • "First class citizen"
  • Elle peut être manipulée comme une expression
  • La construction de modules complexes repose sur l'assemblage de fonctions simples (composition)

Pas d'état

  • Une valeur ne peut être affectée qu'une seule fois
  • Elle ne peut plus être modifiée par la suite
  • => pas de "variables"

Fonctions pures

  • Pas d'effets de bord
  • Transparence référentielle

Récursivité

  • Pas de "variables" donc boucle itérative impossible
  • Remplace les boucles

Fonction de premier ordre

  • Fonction qui prend au moins une autre fonction en paramètre pour fournir un résultat
  • Compositions
  • Développement plus haut niveau

Closures

  • Accès au scope de la fonction parent même après un return
  • L'exécution d'une closure "ressemble" à l'instance d'un objet

Et bien d'autres !

Partial application, Currying, Pattern Matching, Lazy evaluation, Continuations, Monads ...

Disponibilité

  • Certains concepts ne sont utilisables (correctement) que dans des langages fonctionnels
  • D'autres sont parfaitement utilisables dans un langage (moderne) impératif

III. Fonctions de premier ordre

usuelles

Exemples en javascript

map() ou transform()


[1,2,3,4,5].map(function(a) { return a + 1} );
                    

output : [2,3,4,5,6]
                    

reduce() ou foldLeft()


[1,2,3,4,5].reduce(function(memo, a) { return memo + a} );
                    

output : 13
                    
Existe aussi avec reduceRight()

filter()


[1,2,3,4,5].filter(function(a) { return a >= 4 } );
                    

output : [4, 5]
                    

Disponibilité

Natif avec ECMAScript 5

Partout avec underscore.js

Fonctions de premier ordre

Ecrire des boucles manuellement est sujet à erreur

Réduit la quantité de code

Pas seulement pour les collections

Abusez-en !

Java < 1.8 : Guava (tranform(), filter())

PHP : array_map(), array_reduce(), array_filter()

IV. Fonctions pures

Pas d'effet de bord
Pas d'état

Qu'est-ce qu'un effet de bord ?

Modification d'un état externe

I/O, UI etc..

Une application a besoin d'effets de bord mais...

...Les effets de bord

rendent le code difficilement testable

rendent l'ordre d'appel des fonctions primordial

rendent difficile l'exécution parallèle du code (synchronisation, lock, écrasement...)

Que faire ?

Cloisonner les effets de bord

La majorité des fonctions doit être pure

Les effets de bord doivent être limités à une(des) section(s) de l'application

Exemple simple

En PHP

function showNegativeNumber($numberList) {
    foreach($numberList as $number) {
        if($number < 0) echo $number;
    }
}
                    
Comment tester ce code ?

Version "cloisonnée"



function filterNegative($numberList) {
    return array_filter($numberList, function($number) {
        return $number < 0;
    });
}
function showNumbers($numberList) {
    foreach($numberList as $number) {
        echo $number;
    }
}
                    
filterNegative est parfaitement testable
showNumbers peut-être ré-utilisée

Fonctionnel mais ...

Simplement une bonne pratique

Respecte le principe SRP (Single Responsability Principle)

Utilisable partout

V. Transparence référentielle

Exemples en Java


class Foo {

    private List myList = new ArrayList();

    public Foo() { myList.add(1);myList.add(3); }

    public int sumWith(int a) {
        int sum = 0; myList.add(a);
        for(Integer i : myList) sum += i;
        return sum;
    }

    public List getMyList() {
        return myList;
    }
}
                    
Résultat si : foo.sumWith(1) ?
Résultat si :
foo.getMyList().clear();
foo.sumWith(1);

public class FooG {

    public int sumWith(List myList, int a) {
        int sum = 0;
        for(Integer i : myList) sum += i;
        return sum + a;
    }
}
                    
Pour toute valeur de "myList" et "a" équivalente, chaque appel retournera systématiquement la même somme myList n'est jamais modifiée

Java 8


public class FooG {

    public int sumWith(List myList, int a) {
        return myList.reduce( (memo, i) -> memo + i) + a;
    }
}
                    
Plus aucune variable

Transparence référentielle

Limite les effets de bord

Facilite la comprehension du code

Facilite les tests

Une fonction pure est forcément transparente

VI. Closures

Définition

Fonction dans une fonction

Scope de la fonction parente conservée après exécution

Exemple : méthode privée

En javascript

var myObj = function() {

    var myPrivateVar = 10;

    var myPrivateFunc = function() {
        myPrivateVar++;
        return myPrivateVar;
    }

    return {
        myPublicFunc : function() {
            return myPrivateFunc();
        }
    }
};
                    

var o = myObj();
o.myPublicFunc()); //11
o.myPublicFunc()); //12
o.myPrivateFunc()); //o has not method myPrivateFunc()
                    

VII. A quoi ca sert ?

En résumé

Code propre

  • Fonctions courtes qui ne font qu'une chose
  • Ré-utilisation facilitée (fonctions de premier ordre)
  • Simplifie largement le développement...
  • ...En évitant des Design Pattern complexes

Testabilité

  • Fonctions sans effet de bord = test trivial

Maintenabilité

  • Chaque fonction ayant une responsabilité définie...
  • ...Il est plus facile de se réprésenter l'exécution du programme

Productivité

  • Fonctions de premier ordre.
  • Manipulation de collections
  • Ré-utilisation de code
  • Simplification
  • Verbosité

VIII. Pourquoi c'est important ?

C'est l'invasion : ils sont partout

Promise et Future

Javascript, Java, Python, C# etc...

MapReduce

Hadoop (Java)

Traitement de quantité astronomique de données (logs ..)

Parrallélisation du traitement sur plusieurs coeurs et plusieurs serveurs

Architecture non bloquante

Node(Javascript), Playframework (Java, Scala), Servlet 3+ etc..

Actor model

Abstraction au delà des threads/processus

Système distribué, fault-tolerant, daemon, async messaging

Exemples : Akka (Java, Scala), Theron(C++), Erlang (natif)

Et surtout ...

De plus en plus de concepts sont intégrés dans les langages impératifs

Il faut pouvoir en profiter

Des langages hybrides montent en puissance (Javascript, Scala, Ruby)

IX. Comment apprendre ?

Sans avoir fait math sup

Penser différement

Le plus difficile c'est de changer la manière dont on construit le programme

et donc penser différement

On nous a appris à programmer de manière impérative

Casser ses habitudes

Le plus simple pour y parvenir est de s'imerger

Apprendre avec un langage à tendance fonctionnelle très importante

Le(s)quel(s) ?

Langage débutant "friendly"

Langage débutant "friendly"

  • Scala
  • Moins dépaysant (langage hybride)
  • Typage statique
  • Grosse marge de progression, peut-être aussi simple qu'extrêmement complexe
  • Fonctionne sur la JVM, intéropérabilité avec librairies Java
  • Technologies qui font le "buzz" Playframework, Akka etc...

Apports personnel

  • Voir les mêmes problèmes sous un autre angle
  • Maîtriser les concepts fonctionnels vous aidera à écrire un meilleur code impératif
  • Ajouter un nouvel outil dans votre boite à outil
  • X. Quelques exemples

    En Scala

    For comprehension

    
    for(
        p <- persons
        if !p.male
        c <- p.children
    ) yield (p.name, c.name)
                        
    <=>
    
    persons.filter(p => !p.male)
           .flatMap(p => (
                p.children.map( c => (p.name, c.name))
           ))
                        
    La version Java rentre pas dans une slide :)

    Parallélisation

    
    bigList.par.map(line => processItem(line))
                        

    Le type OPTION

    En finir avec les NullPointerException
    Une collection d'un élément soit égal à Some(value) soit à None
    Pourquoi une collection ?
    Parce que remplacer if(!null) par if(!None) ca nous avance pas beaucoup

    Le type Option

    
    def findById(id: Int): Option[User] = users.get(id)
                        
    Récupérer une valeur sans se préocuper de si la valeur est nulle ou pas
    
    findById(1).map(_.age) // Some(valeurAge)
                        
    Et si l'age peut être lui même "null" ?
                            
    findById(1).map(_.age) // Option[Option[Int]]
    findById(1).flatMap(_.age) // Some(valeuAge)
    //ou encore
    for(user <- findById(1); age <- user.age) yield (age)
    //filtrage
    findById(1).filter(_.age > 30) // Some(valeurAge) si condition remplie
                            
                        

    Future et Promise

    Imaginez une série d'actions asynchrones, mais avec des dépendances En impératif, on utilisera des callback Si le nombre d'opérations est important on arrive rapidemment à :

    Le type Future

    Une collection également
    
    def doStep1():Future[Int] = ...
    for {
        step1 <- doStep1()
        step2 <- doStep2()
        step3 <- doStep3()
        step4 <- doStep4(step1, step3) // seul exécution déterministe,
                            //nécessite des valeurs des futures précédentes
    } yield combine(step1, step3)
                        

    XI. Ressources

    Général

  • Functional Programming For The Rest of Us
  • Why Functional Programming Matters
  • Functional programming is a ghetto
  • Scala VS Java 8
  • Functional techniques used in imperative languages?
  • Javascript

  • Eloquent Javascript
  • Functionnal Javascript
  • underscorejs
  • PHP

  • PHP the right way
  • State of functionnal programming in PHP
  • Functionnal programming in PHP
  • Functionnal programming in PHP (bis)
  • Java

  • Functionnal Java
  • Guava
  • Java 8 lambda
  • Le mot de la fin

    
    def apply[B](f1: (A1, A2, A3) => B, f2: B => (A1, A2, A3))
                        (implicit fu: InvariantFunctor[M]): M[B] =
        fu.inmap[A1 ~ A2 ~ A3, B](
            canBuild(m1, m2), { case a1 ~ a2 ~ a3 => f1(a1, a2, a3) },
            (b: B) => { val (a1, a2, a3) = f2(b); new ~(new ~(a1, a2), a3)}
         )