Introduction aux combinateurs
Les combinateurs sont des fonctions de niveau supérieur qui permettent de combiner des fonctions, des variables ou d'autres combinateurs. En général, ils ne contiennent pas de déclarations de leurs propres variables ou de leur propre logique de gestion. Ils sont les seuls à permettre de rincer le contrôle dans un programme de fonctions.
Dans cet article, j'essaierai d'en couvrir quelques-uns :
- Robinet
- Curry
- Pipe/Composition
- Fourchette
- Alternance
- Séquence
Robinet
Un combinateur est très utile pour les fonctions qui ne renvoient rien. Il prend la fonction à laquelle le paramètre est destiné et la renvoie ensuite.
Déclaration
const tap = (fn) => (value) => {
fn(valeur) ;
retourne la valeur ;
} ;
Exemple d'impératif
const [items, setItems] = useState(() => [])
axios
.get('http://localhost')
.then({ data } => {
setItems(data)
console.log(data)
onLoadData(data)
}).then(...) // renvoie undefined - les données de la promesse ont été modifiées
Exemple déclaratif
const [items, setItems] = useState(() => [])
axios
.get('http://localhost')
.then(({ data }) => data)
.then(tap(setItems)) // (data) => { setItems(data) ; return data }
.then(tap(console.log)) // un then = une fonction = une responsabilité
.then(tap(onLoadData))
.then(...) // toujours accès aux données d'origine
// facile de maintenir le principe d'ouverture/fermeture
Curry
Il divise les arguments d'une fonction et permet de les appeler de manière séquentielle.
Déclaration
const curry = (fn) => {
const curried = (...args) => {
if (fn.length !== args.length){
return curried.bind(null, ...args)
}
return fn(...args) ;
} ;
return curried
} ;
Exemple
const curry = (fn) => {
const curried = (...args) => {
if (fn.length !== args.length){
return curried.bind(null, ...args)
}
return fn(...args) ;
} ;
return curried
} ;
const sum = (a, b, c) => a + b + c
const currySum = curry(sum)
/*
appels possibles
currySum(a)(b)(c),
currySum(a)(b,c),
currySum(a,b)(c),
currySum(a,b,c)
*/
currySum(1) // (b, c) => 1 + a + b ou (b) => (c) => 1 + a + b
currySum(5)(10) // (c) => 5 + 10 + b
currySum(5, 10) // (c) => 5 + 10 + b
currySum(5)(10)(20) // 35
currySum(5, 10)(20) // 35
currySum(5)(10, 20) // 35
const divideBy = curry((a, b) => b / a)
const multiplyBy = curry((a, b) => a * b)
const divideByTwo = divideBy(2)
divideByTwo(10) // renvoie 5
const multiplyByFive = multiplyBy(5)
multiplyByFive(10) // renvoie 50
Pipe/Composition
Grâce à la composition, il est possible de passer des données d'une fonction à une autre. Il est important que les fonctions prennent le même nombre d'arguments. La différence entre pipe et compose est que le premier exécute la fonction de la première à la dernière, tandis que compose les appelle à partir de la fin.
Déclaration
const pipe = (...fns) => (value, ...args) =>
fns.reduce((v, f, i) =>
i === 0
? f(v, ...args)
: f(v),
valeur) ;
const compose = (...fns) => (value, ...args) =>
fns.reduceRight((v, f, i) =>
i === (fns.length - 1)
? f(v, ...args)
: f(v),
valeur) ;
const pipe = (...fns) => (value, ...args) =>
fns.reduce((v, f, i) =>
i === 0
? f(v, ...args)
: f(v),
valeur) ;
const compose = (...fns) => (value, ...args) =>
fns.reduceRight((v, f, i) =>
i === (fns.length - 1)
? f(v, ...args)
: f(v),
valeur) ;
Exemple d'impératif
const sine = (val) => Math.sin(val * Math.PI / 180) // non lisible
sine(90) // renvoie 1
Exemple déclaratif
const sine = pipe(
multiplyBy(Math.PI) // ↓ val * Math.PI
divideBy(180), // ↓ val * Math.PI / 180
Math.sin, // ↓ Math.sin(val * Math.PI / 180)
)
const sine = compose(
Math.sin, // ↑ Math.sin(val * Math.PI / 180)
divideBy(180), // ↑ val * Math.PI / 180
multiplyBy(Math.PI) // ↑ val * Math.PI
)
sine(90) // renvoie 1
Fourchette
Le combinateur de fourche est utile dans les situations où vous souhaitez traiter une valeur de deux manières différentes et combiner les résultats.
Déclaration
const fork = (join, fn1, fn2) => (value) => join(fn1(value), fn2(value)) ;
Exemple
const length = (array) => array.length
const sum = (array) => array.reduce((a, b) => a + b, 0)
const divide = (a, b) => a / b
const arithmeticAverage = fork(divide, sum, length)
arithmeticAverage([5, 3, 2, 8, 4, 2]) // renvoie 4
Alternance
Ce combinateur prend deux fonctions et renvoie le résultat de la première si elle est vraie. Sinon, il renvoie le résultat de la seconde fonction.
Déclaration
const alt = (fn, orFn) => (valeur) => fn(valeur) || orFn(valeur)
Exemple
const users = [{
uuid: '123e4567-e89b-12d3-a456-426655440000',
name : 'William'
}]
const findUser = ({ uuid : searchesUuid }) =>
users.find(({ uuid }) => uuid === searchesUuid)
const newUser = data => ({ ...data, uuid : uuid() // créer un nouvel uuid })
const findOrCreate = alt(findUser, newUser)
findOrCreate({ uuid : '123e4567-e89b-12d3-a456-426655440000' }) // renvoie l'objet William
findOrCreate({ name : 'John' }) // renvoie l'objet John avec un nouvel uuid
Séquence
Il accepte de nombreuses fonctions indépendantes et transmet le même paramètre à chacune d'entre elles. En général, ces fonctions ne renvoient aucune valeur.
Déclaration
const seq = (...fns) => (val) => fns.forEach(fn => fn(val))
Exemple
const appendUser = (id) => ({ name }) => {
document.getElementById(id).innerHTML = name
}
const printUserContact = pipe(
findOrCreate, // renvoie l'utilisateur
seq(
appendUser('#contact'), // utilisateur => void
console.log, // utilisateur => void
onContactUpdate // utilisateur => void
)
)
printUserContact(userData)
