title | theme | highlightTheme | revealOptions | ||
---|---|---|---|---|---|
Intro To Functional Programming |
solarized |
atom-one-dark |
|
Slides: https://gvergnaud.github.io/intro-to-functional-programming
Gabriel Vergnaud
HĂ©ticien de la P2017
Note:
- Qui suis je ?
- gabriel vergnaud
- Heticien P2017
- developer Ă Sketchfab.com (On recrute!)
- gvergnaud on github
- GabrielVergnaud on twitter
Qui ĂŞtes vous ?
Note:
- Qui etes vous ?
- Techno utilisée ?
- quels projets ?
- plutot agence / produits ?
- Dev back vs dev front ?
Note: Quel genre de cours de dev j'avais besoin et envie quand j'étais en H3 ? appris en auto-didacte. Connaissances empirique Je faisais des applications de plus en plus complexe, mais il arrivé toujours un moment ou cette complexité devenait un peu hors de control. ça devenait compliqué d'ajouter des features sans créer des bugs, le code devenait difficile à lire...
J'avais envie d'approfondir mes connaissances pour éviter de tomber dans ces pièges. -> L'architecture d'application.
Au dela de react, a travers les quelques jours de cours que j'ai avec vous, j'ai envie de vous transmettre ce que j'ai découvert ces dernières années en matière d'architecture d'application.
J'étais sensé vous parler de React...
Mais d'abord...
Note:
J'ai beaucoup hésité, j'ai préparé un cours sur react au début et tout
Mais je me suis rendu compte que pour vous parler de ce dont je dois vous parler je dois introduire un certain nombre de concepts.
Un peu de théorie
Note:
ça fait peur, mais c'est cette partie science dans computer science qui fait qu'on fait un métier digne d'intérêt et que l'on est pas des machines. Il faut pas avoir peur de la théorie.
Slides: https://gvergnaud.github.io/intro-to-functional-programming
- Comprendre ce qui se cache sous le terme functional programming
- Donner des pistes Ă explorer
De nouveaux frameworks tous les ans
Ceux des années précédentes deviennent obsolètes
Que doit on apprendre qui nous serve Ă long terme ?
ArrĂŞter de passer du temps Ă apprendre des **API**
Mieux comprendre les fondamentaux du langage
Se concentrer sur les **design patterns**
[https://www.youtube.com/watch?v=ZZUY37RQS-k](https://www.youtube.com/watch?v=ZZUY37RQS-k)
un code avec
parce que plus facile Ă comprendre
parce que plus facile à réutiliser
“Developers proficient in functional programming are going to be in large demand in the very near future.”
Eric Elliott - The Two pillars of Javascript - Part 2
Lisp, ML,
Haskell, Scala, Erlang, Elixir, Clojure, OCaml…
Qui compilent en JS
Reason, Elm, ClojureScript
Dans une moindre mesure
le JS
on peut faire du fonctional dans presque tous les languages.
Ruby, Python, ..., mĂŞme PHP !
Préférer un style **déclaratif** à un style impératif
+
découper son code en petites **fonctions** réutilisables et composables.
+
Ne **jamais** mutter les données
Note: Ça permet de monter facilement en abstraction et d’arreter de tout micro manager.
Impératif :
const users = [{ age: 20 }, { age: 31 }, { age: 17 }]
let oldUsers = []
for (let i = 0; i < users.length; i++) {
const currentUser = users[i]
if (currentUser.age > 30) oldUsers.push(currentUser)
}
console.log(oldUsers)
// => [{ age: 31 }]
Oulala qu'est ce que ça micromanage
Note:
Micro management =
- déclaration de variables temporaire,
- for loops,
- if statements
- déclaration de variables temporaires
- mutation de données
- des statements
- for loops
- if/else statements
DĂ©claratif :
const oldUsers = filter(user => user.age > 30, users)
console.log(oldUsers)
// => [{ age: 31 }]
DĂ©claratif :
const oldUsers = filter(user => user.age > 30, users)
console.log(oldUsers)
// => [{ age: 31 }]
On va décrire le comportement du programme
plutĂ´t que de dire ce qu'il se passe
Ă chaque Ă©tape de la boucle.
Mais ou est donc définie la function filter ?
Le déclaratif est construit sur de l’impératif.
function filter(predicate, xs) {
let out = []
for (let i = 0; i < xs.length; i++) {
if (predicate(xs[i])) out.push(xs[i])
}
return out
}
`filter` est ce qu'on appelle une **Higher Order Function**
“Its like the word Quintessential :
when you say it, it just makes you feel smart.”
@mpjme
Youtube - Higher Order Functions
Une fonction qui :
va **abstraire** un bout de code **générique**
va prendre une **autre** fonction en paramètre
qui **décrit** le comportement **spécifique** de notre code.
(un peu)
On peut aussi faire des boucles de manière déclarative
const filter = (f, [x, ...rest]) =>
rest.length
? f(x) ? [x, ...filter(f, rest)] : filter(f, rest)
: f(x) ? [x] : []
grâce à la recursion !
Filter est build-in sur Array.prototype
const oldUsers = users.filter(user => user.age > 30)
C'est à dire transformer chacun des items d'un tableau pour en créer un nouveau.
Impératif :
const users = [{ age: 20 }, { age: 31 }, { age: 17 }]
let userAges = []
for (let i = 0; i < users.length; i++) {
userAges.push(users[i].age)
}
console.log(userAges)
// => [20, 31, 17]
Déclaratif : la méthode map()
const userAges = users.map(user => user.age)
console.log(userAges)
// => [20, 31, 17]
OU
const userAges = map(user => user.age, users)
console.log(userAges)
// => [20, 31, 17]
implémentation de map
function map(mapper, xs) {
let out = []
for (let i = 0; i < xs.length; i++) {
out.push(mapper(xs[i]))
}
return out
}
ou avec recursion
const map = (f, [x, ...rest]) =>
rest.length ? [f(x), ...map(rest)] : [f(x)]
C'est Ă dire transformer les items d'un tableau en une seule valeur.
Impératif :
const numbers = [20, 31, 17]
let sum = 0
for (let i = 0; i < numbers.length; i++) {
sum = sum + numbers[i]
}
console.log(sum)
// => 68
Déclaratif : la méthode reduce()
const sum = numbers.reduce((acc, num) => acc + num, 0)
console.log(sum)
// => 68
OU
const sum = reduce((acc, num) => acc + num, 0, numbers)
console.log(sum)
// => 68
Implémentation de reduce
function reduce(reducer, seed, xs) {
let out = seed
for (let i = 0; i < xs.length; i++) {
out = reducer(out, xs[i])
}
return out
}
Ou avec recursion
const reduce = (f, acc, [x, ...rest]) =>
rest.length ? reduce(f, f(acc, x), rest) : f(acc, x)
C'est maintenant que ça prend tout son sens...
// users :: [ { name :: String, age :: Int } ]
const name = 'Kevin'
let averageAge = 0
let filteredUsers = []
for (let i = 0; i < users.length; i++) {
const user = users[i]
if (user.name === name) filteredUsers.push(user)
}
for (let j = 0; j < filteredUsers.length; j++) {
const user = filteredUsers[j]
averageAge = averageAge + user.age / filteredUsers.length
}
Qu'est ce que ça fait en un coup d'oeil ?
const averageAge = users
.filter(user => user.name === 'Kevin')
.map(user => user.age)
.reduce((acc, age, _, ages) => acc + age / ages.length, 0)
Et maintenant ?
- L'Impératif, visuellement, c'est pas facile.
la spécificité du bout de code est noyée dans les détails d'implémentation génériques.
- Impératif = code peu réutilisable
nos boucles sont spécifiquement faite pour notre problème actuel.
- Impératif = code complexe
Rich Hickey - [Simple Made Easy](https://www.youtube.com/watch?v=rI8tNMsozo0)
<iframe src="https://codesandbox.io/embed/exercice-fp-declarative-vs-imperative-lrion?fontsize=14&view=editor" title="Exercice FP declarative vs imperative" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
fonctions pures et impures
Une fonction pure ne produit pas de side effect.
Une fonction pure retourne le mĂŞme resultat si on lui donne les mĂŞmes arguments.
Une fonction pure est une fonction au sens mathématique du terme.
Note:
- Dans le monde de la programmation fonctionelle on appelle ça du code Impure
- Une fonction pure n'a pas de side effect et donne le même résultat si on lui donne le arguments.
- on dit qu'elle est référentiellement transparente (referencial transparency).
I hate maths
prend des paramètres et retourne un résultat
// Pure
const add = (a, b) => a + b
add(2, 2)
// => 4
// Impure
const add = (a, b, cb) => {
cb(a + b)
}
add(2, 2, v => console.log(v))
// => undefined
// "4"
n'altère pas les paramètres qui lui sont passés
// Pure
const addValue = (arr, v) => arr.concat(v)
addValue([1, 2, 3], 4)
// => [1, 2, 3, 4]
// Impure
const addValue = (arr, v) => {
arr.push(v)
}
let myArray = [1, 2, 3]
addValue(myArray, 4)
// => undefined
// myArray === [1, 2, 3, 4]
ne produit pas de side effect
const number = 3
// Pure
const isOdd = x => !(x % 2)
isOdd(number)
// => true
// Impure
let isNumberOdd;
const isOdd = () => {
isNumberOdd = !(number % 2)
}
isOdd() // => undefined
console.log(isNumberOdd) // => true
// Impure
const getUsers = (callback) => {
fetch(`/user`).then(callback)
}
On peut **tout comprendre** juste en regardant le contenu de la fonction
car elle ne dépend pas du monde extérieur.
Un comportement consistant, donc moins de bugs.
Puisqu'une fonction pure retourne le **même resultat** si on lui donne les **mêmes arguments**,On peut **memoize** le resultat! **memoize** est une manière d'optimiser les performances en gardant le resultat en mémoire pour **éviter de ré-executer** le code si la fonction est appelée avec les **mêmes arguments**.
const isThisPure = x => x * 2
alors ? pure ou pas pure ?
Pure
const isThisPure = str => {
window.title = str
}
Impure
const isThisPure = str => {
const title = `AppName - ${str}`
return () => window.title = title
}
Pure
Retourner une fonction impure sans l'exécuter ne rend pas la fonction impure.Ce qui nous intéresse dans un programme c'est les side effects.
**Afficher** quelque chose sur la page
**RĂ©agir** Ă des events
**Écrire** dans une base de données
On va pas loin avec des fonctions pures.
Mais les side effects sont aussi les plus susceptibles de bugger.
SideEffect -> Pure -> SideEffect
Pousser les side effects aux **extrémités** de notre code.
Note:
Demo ->
getData().then(app)
const app = compose(
render
map(etc)
)
Par exemple :
GetData -> Computation -> Render
Computation : la **logique** de notre app
const app = () => render(compute(getData()))
Ca vous rappelle pas quelque chose ?
const app = () => view(controller(model()))
On retrouve les trois parties du model **MVC**
les **données**, le **traitement** et la **vue**.
Soit deux fonction
f(x)
etg(x)
leur function composée est
f(g(x))
L'idée est de connecter les functions entre elles
en passant le resultat d'un fonction en paramètre à la suivante.
import { compose } from 'ramda';
const double = x => x * 2
const tripple = x => x * 3
// composition ->
const sixtuple = compose(double, tripple)
console.log(sixtuple(7))
// 42
Pareil que
const double = x => x * 2
const tripple = x => x * 3
const sixtuple = x => double(tripple(x))
console.log(sixtuple(7))
// 42
Pareil que
const sixtuple = x => {
const trippled = tripple(x)
const result = double(trippled)
return result
}
console.log(sixtuple(7))
// 42
Comme si on emboitait des légos !
“The way to control Complexity
is Compositionality.”
Brian Beckman: Don't Fear The Monad
Signature de function qui décrit son type
multiplyByTwo :: Int -> Int
const multiplyByTwo = x => x * 2
_
length :: String -> Int
const length = str => str.length
_
Il y a même un moteur de recherche pour ça :
Haskell Function Search Engine
C'est très pratique pour composer des functions.
length :: String -> Int
const length = str => str.length
isGreaterThanTen :: Int -> Bool
const isGreaterThanTen = x => x > 10
quel sera la signature de
compose(isGreaterThanTen, length)
?
const isLengthGreaterThanTen = compose(length, isGreaterThanTen)
(String -> Int) + (Int -> Bool) = String -> Bool
donc
isLengthGreaterThanTen :: String -> Bool
comment on fait quand les functions prènnent plusieurs arguments ?
la solution Ă tous nos soucis
une fonction curriée est une function qui, lorsqu'on ne lui donne pas tous ses paramètres, renvoie une function qui prendra le reste.
import { curry } from 'ramda';
const sumOfFour = (a, b, c, d) => a + b + c + d
sumOfFour(1)
// => NaN
const curriedSumOfFour = curry(sumOfFour)
curriedSumOfFour(1)
// => (b, c, d) => 1 + b + c + d
_
Presque pas besoin :
const sumOfFour = a => b => c => d => a + b + c + d
sumOfFour(1)
// => (b, c, d) => 1 + b + c + d
_
en ES6, les arrow functions
facilitent la syntaxe
des functions qui retournent des functions.
Mais la function curry
nous permet de passer
aussi plusieurs paramètres d'un coup si on veut :
const sumOfFour = curry((a, b, c, d) => a + b + c + d)
sumOfFour(1, 2)
// => (c, d) => 1 + 2 + c + d
const sumOfFour = a => b => c => d => a + b + c + d
sumOfFour(1, 2)
// => (b, c, d) => 1 + b + c + d
// :(
_
<iframe src="https://codesandbox.io/embed/exercice-fp-declarative-vs-imperative-46rr9?fontsize=14&view=editor" title="Exercice FP curry and compose" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
En functional programming lorsque l'on assigne une variable, ça veut dire qu'elle ne changera plus jamais.
Les données immutables c'est bien
parce ce que ça supprime de l'incertitude.
l'utilisation de const
ne rend pas la donnée immutable.
Cela empèche juste que la variable puisse être ré-assignée.
Ou pourra toujours modifier les items d'un array ou les clés d'un objet.
Comment Ă©viter de mutter un tableau ?
ne pas utiliser push()
const immutablePush = (x, xs) => xs.concat(x)
// OU
const immutablePush = (x, xs) => [ ...xs, x]
_
modifier un item :
const immutableSetAtIndex = (index, value, xs) => [
...xs.slice(0, index),
value,
...xs.slice(index + 1)
]
_
Comment Ă©viter de mutter un object ?
const objectSet = (key, value, obj) =>
Object.assign({}, obj, { [key]: value })
// OU
const objectSet = (key, value, obj) =>
({ ...obj, [key]: value })
_
Une api plus friendly pour faire de l'immutable.
3 méthodes : view
, over
et set
.
// view :: Lens -> DataStructure -> a
view(lens, dataStructure)
// over :: Lens -> (a -> a) -> DataStructure -> DataStructure
over(lens, mapper, dataStructure)
// set :: Lens -> a -> DataStructure -> DataStructure
set(lens, value, dataStructure)
_
Immutable.js
Mon ptit nom c'est Gabriel Vergnaud
<style> .flex { display: flex; align-items: center; justify-content: center; } .flex > *:not(:first-child) { margin-left: 10px } img.simple-image.simple-image { border:none; box-shadow:none; background: none; } .white.white.white { color: white; text-shadow: 0 2px 4px rgba(0,0,0, .5); } .lower.lower.lower { text-transform: none; } .reveal { font-size: 32px; } .reveal small { font-size: 0.7em; line-height: 1.5; } .reveal pre { border-radius: 5px; box-shadow: 0px 8px 25px rgba(0,0,0,.25); } .reveal section img { border: none; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15) } .reveal pre code { padding: 30px; border-radius: 5px; font-weight: normal; } .reveal code { font-weight: bold; } </style>