Comprendre la programmation fonctionnelle, Blend Web Mix le 02/11/2016

Preview:

Citation preview

Comprendre la programmation fonctionnelle

@loicknuchel

Loïc KnuchelGeek passionné

Développeur Scala

Organisateur

Débuter avec la programmation fonctionelle ?

High-order function

Pure function

Immutable

Functor

Currying

Monad

Applicative

RécursifMonoid

Au fait, c’est quoi la programmation fonctionnelle ?

“La programmation fonctionnelle est un paradigme de programmation qui considère le calcul en tant qu'évaluation de fonctions mathématiques.”

Wikipedia

“La programmation fonctionnelle est un style de programmation qui met l’accent sur les fonctions qui ne dépendent pas de l’état du programme.”

Functionnal programming in scala

“La programmation fonctionnelle permet de coder de manière plus productive, plus modulaire et avec moins de bugs.”

Loïc Knuchel ;)

Exemple ?

for loops are everywhere

function toUpperCase(list){ const ret = []; for(let i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } return ret;}

console.log(toUpperCase(['Finn', 'Rey', 'Poe']));// ['FINN', 'REY', 'POE']

for loops are everywhere

function toUpperCase(list){ const ret = []; for(let i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } return ret;}

console.log(toUpperCase(['Finn', 'Rey', 'Poe']));// ['FINN', 'REY', 'POE']

Interdiction de modifier les paramètres !!!

for loops are everywhere

function toUpperCase(list){ const ret = []; for(let i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } return ret;}

console.log(toUpperCase(['Finn', 'Rey', 'Poe']));// ['FINN', 'REY', 'POE']

Boilerplate !!!

for loops are everywhere

Array.prototype.map = function(transform){ const array = this; const result = []; for(let i=0; i<array.length; i++){ result[i] = transform(array[i]); } return result;};

for loops are everywhere

Array.prototype.map = function(transform){ const array = this; const result = []; for(let i=0; i<array.length; i++){ result[i] = transform(array[i]); } return result;};

Fonction d’ordre supérieur

for loops are everywhere

function toUpperCase(list){ const ret = []; for(let i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } return ret;}

function toUpperCase(list){ return list.map(item => item.toUpperCase());}

Séparation technique vs métier

Array.prototype.map = function(transform){ var array = this; var result = []; for(var i=0; i<array.length; i++){ result[i] = transform(array[i]); } return result;};

list.map(item => item.toUpperCase());

Générique / Réutilisable / StableAbstraction de haut niveau

Concis / Expressif / FlexibleFocalisé sur le domaine

Cas pratique

Température moyenne par localisation

const data = [ { coords: [42.097002, -79.235326], temperatures: [-34, 67, 101, 87] }, { coords: [38.888025, -121.016225], temperatures: [-3, 4, 9, 12] }, { coords: [40.462512, -99.249261], temperatures: [75, 75, 75, 75, 75] }, ...];

// expected format :[ // [coords, average temperature] [[42.097002, -79.235326], 55.25], [[38.888025, -121.016225], 5.5], [[40.462512, -99.249261], 75], ...]

Code impératif

function chartFormat(data){ var results = [], totalTemp = 0, averageTemp = 0; for(var i=0; i < data.length; i++) { totalTemp = 0; for(var j=0; j < data[i].temperatures.length; j++) { totalTemp += data[i].temperatures[j]; } averageTemp = totalTemp / data[i].temperatures.length; results.push([data[i].coords, averageTemp]); } return results;}

● Difficile à comprendre

● Pas réutilisable / générique

● Bugs probables

● Difficile à tester

Fonctionnel

● Immutabilité

Meilleure compréhension du codeFixe les problèmes :● Asynchrone● Concurrent● Scalabilité horizontale

Fonctionnel

● Immutabilité

● Stateless Raisonnement local

Couplage réduit

Testabilité

Fonctionnel

● Immutabilité

● Stateless

● Pas d’effet de bord

Effet de bord :● faire un appel (bdd, http, fichier…)● récupérer la date actuelle● accéder à une variable “globale”● modifier un paramètre● lancer une exception● afficher un log● ...

Fonctionnel

● Immutabilité

● Stateless

● Pas d’effet de bord

Raisonnement local

Couplage réduit

Composition facilitée

Testabilité

Fonctionnel

● Immutabilité

● Stateless

● Pas d’effet de bord

Functional core / Imperative shell

Fonctionnel

● Immutabilité

● Stateless

● Pas d’effet de bord

● Décomposer en fonction réutilisables

Moyenne des températures par point

Fonctionnel : moyenne des températures

function sum(numArr, currentTotal){ currentTotal = currentTotal || 0; if(numArr.length === 0){ return currentTotal; } else { return sum(numArr.slice(1), currentTotal + numArr[0]); }}

function avg(numArr){ return sum(numArr) / numArr.length;}

var averageTemp = avg(temperatures);

Fonctionnel : extraire les températures

var allTemperatures = data.map(function(item){ return item.temperatures;});

const data = [ { coords: [42.097002, -79.235326], temperatures: [-34, 67, 101, 87] }, { coords: [38.888025, -121.016225], temperatures: [-3, 4, 9, 12] }, { coords: [40.462512, -99.249261], temperatures: [75, 75, 75, 75, 75] }, ...];

Curryfication

function add1(b){ return 1+b;}

console.log(add1(2)); // 3

function addCurry(a){ return function(b){ return a+b; }}

var add1 = addCurry(1);var add2 = addCurry(2);

console.log(add1(2)); // 3console.log(add2(2)); // 4

Fonctionnel : extraire les températuresvar allTemperatures = data.map(function(item){ return item.temperatures;});

function getAttr(attrName){

return function(item){

return item[attrName];

}

}

var allTemperatures = data.map(getAttr('temperatures'));

function mapAttr(arr, attrName){

return arr.map(getAttr(attrName));

}

Array.prototype.mapAttr = function(attrName){ return mapAttr(this, attrName);};var allTemperatures = data.mapAttr('temperatures');

Fonctionnel : combiner nos données

var coordsList = data.mapAttr('coords'); // [[42.097, -79.235], ...]var avgTemps = data.mapAttr('temperatures').map(avg); // [55.25, 5.5, 75, ...]

function zip(arr1, arr2, resultArr){ resultArr = resultArr || []; if(arr1.length === 0 || arr2.length === 0){ return resultArr; } else { return zip(arr1.slice(1), arr2.slice(1), resultArr.concat([arr1[0], arr2[0]])); }}// zip([1, 2, 3], [‘a’, ‘b’, ‘c’]) => [[1, ‘a’], [2, ‘b’], [3, ‘c’]]

Array.prototype.zip = function(other){ return zip(this, other, []);};

var chartData = coordsList.zip(avgTemps);// [ [[42.097002, -79.235326], 55.25] , [[38.888025, -121.016225], 5.5], ... ]

Fonctionnel : tout combiner

function chartFormat(data){ return data.mapAttr('coords').zip(data.mapAttr('temperatures').map(avg));}

VS

function chartFormat(data){ var results = [], totalTemp = 0, averageTemp = 0; for(var i=0; i < data.length; i++) { totalTemp = 0; for(var j=0; j < data[i].temperatures.length; j++) { totalTemp += data[i].temperatures[j]; } averageTemp = totalTemp / data[i].temperatures.length; results.push([data[i].coords, averageTemp]); } return results;}

def chartFormat(data: List[((Double, Double), List[Double])]) = data.map(_._1).zip(data.map(_._2).map(t => t.sum / t.length))

Bilan

● Plus facile à :○ écrire○ lire○ maintenir

● Beaucoup moins de bugs

Scala collection API (petite partie)

def map[B](f: (A) => B): List[B]def filter(p: (A) => Boolean): List[A]def partition(p: (A) => Boolean): (List[A], List[A])def zip[B](that: List[B]): List[(A, B)]def sliding(size: Int): Iterator[List[A]]

def find(p: (A) => Boolean): Option[A]def exists(p: (A) => Boolean): Boolean

def flatten[B]: List[B]def flatMap[B](f: (A) => List[B]): List[B]

def groupBy[K](f: (A) => K): Map[K, List[A]]def grouped(size: Int): Iterator[List[A]]

def fold[A1 >: A](z: A1)(op: (A1, A1) => A1): A1def reduce[A1 >: A](op: (A1, A1) => A1): A1def forall(p: (A) => Boolean): Boolean

def take(n: Int): List[A]def drop(n: Int): List[A]def distinct: List[A]

Bugs are everywhere...

function toUpperCase(list){ const ret = []; for(let i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } return ret;}

Safe ?

Bugs are everywhere...

function toUpperCase(list){ const ret = []; for(let i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } return ret;}

Unsafe !

Cannot read property 'xxx'

of undefined !!!

Bugs are everywhere...

function toUpperCase(list){ const ret = []; for(let i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } return ret;}

function toUpperCase(list){ var ret = []; if(Array.isArray(list)) { for(var i=0; i<list.length; i++){ if(typeof list[i] === 'string'){ ret[i] = list[i].toUpperCase(); } else { throw "not a string"; } } } else { throw "not an array"; } return ret;}

Bugs are everywhere...

function toUpperCase(list){ const ret = []; for(let i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } return ret;}

function toUpperCase(list){ var ret = []; if(Array.isArray(list)) { for(var i=0; i<list.length; i++){ if(typeof list[i] === 'string'){ ret[i] = list[i].toUpperCase(); } else { ret[i] = list[i]; } } } return ret;}

Bugs are everywhere...

function toUpperCase(list){ const ret = []; for(let i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } return ret;}

function toUpperCase(list){ var ret = []; if(Array.isArray(list)) { for(var i=0; i<list.length; i++){ if(typeof list[i] === 'string'){ ret[i] = list[i].toUpperCase(); } else { ret[i] = list[i]; } } } return ret;}

Unreadable !

Bugs are everywhere...

function toUpperCase(list){ const ret = []; for(let i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } return ret;}

function toUpperCase(list){ var ret = []; if(Array.isArray(list)) { for(var i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } } return ret;}

function toUpperCase(list){ var ret = []; if(Array.isArray(list)) { for(var i=0; i<list.length; i++){ if(typeof list[i] === 'string'){ ret[i] = list[i].toUpperCase(); } else { ret[i] = list[i]; } } } return ret;}

Bugs are everywhere...

function toUpperCase(list){ const ret = []; for(let i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } return ret;}

function toUpperCase(list){ var ret = []; if(Array.isArray(list)) { for(var i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } } return ret;}

function toUpperCase(list){ var ret = []; if(Array.isArray(list)) { for(var i=0; i<list.length; i++){ if(typeof list[i] === 'string'){ ret[i] = list[i].toUpperCase(); } else { ret[i] = list[i]; } } } return ret;}

not so “smart”Cannot read property 'xxx'

of undefined !!!

Bugs are everywhere...

function toUpperCase(list){ const ret = []; for(let i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } return ret;}

function toUpperCase(list){ var ret = []; if(Array.isArray(list)) { for(var i=0; i<list.length; i++){ ret[i] = list[i].toUpperCase(); } } return ret;}

function toUpperCase(list){ var ret = []; if(Array.isArray(list)) { for(var i=0; i<list.length; i++){ if(typeof list[i] === 'string'){ ret[i] = list[i].toUpperCase(); } else { ret[i] = list[i]; } } } return ret;}

Unsafe !

not so “smart”

Unreadable !

Option

Option

val myMap = Map("key" -> "value")

val v1: Option[String] = myMap.get("key") // Some("value")

val v2: Option[String] = myMap.get("miss") // None

val v3: Option[String] = v1.map(_.toUpperCase) // Some("VALUE")

val v4: Option[String] = v2.map(_.toUpperCase) // None

val v5: String = v3.getOrElse("default") // "VALUE"

val v6: String = v4.getOrElse("default") // "default"

Option

def toUpperCase(list: List[String]) = list.map(_.toUpperCase)

def toUpperCase(list: List[Option[String]]) = list.map(_.map(_.toUpperCase))

def toUpperCase(list: Option[List[Option[String]]]) = list.map(_.map(_.map(_.toUpperCase)))

List.map() vs Option.map() ?

List.map() vs Option.map() ?

Fonctors !!!

def toWords(sentences: List[String]): List[List[[String]] = sentences.map(_.split(" ").toList)

def toWords(sentences: List[String]): List[String] = sentences.flatMap(_.split(" ").toList)

def toWords(sentences: List[String]): List[List[[String]] = sentences.map(_.split(" ").toList)

def toWords(sentences: List[String]): List[String] = sentences.flatMap(_.split(" ").toList)

Applicative !

def toWords(sentences: List[String]): List[List[[String]] = sentences.map(_.split(" ").toList)

def toWords(sentences: List[String]): List[String] = sentences.flatMap(_.split(" ").toList)

Applicative !

Fonctor + Applicative = Monad

def toWords(sentences: List[String]): List[List[[String]] = sentences.map(_.split(" ").toList)

def toWords(sentences: List[String]): List[String] = sentences.flatMap(_.split(" ").toList)

Future[A]Option[A]

List[A]Try[A]

Page[A]

Monads !!!

Level up your abstractions !

Take away

● Paramètre de fonction plutôt que donnée globale (même de classe)

● Créer des objets plutôt que de les modifier (immutable)

● Option plutôt que ‘null’

● Option / Try / Either / Validation plutôt qu’une exception

● Collection API / récursivité plutôt que boucles for/while

● Eviter les ‘if’ autant que possible

● Séparation technique / métier

● Functionnal core / Imperative shell